Merge backend-overhaul to master (#3198)

* Migrated logEvents to retrofit (#3087)

* Switched wikimedia-android-data-client for new features

* Added UserCLient and UserInterface

* Migrated ContributionsSyncAdapter to UserClient
Fixed sync related bugs

* Removed unused code

* Removed unused code

* Updated wikimedia-android-data-client to new version

* Update library for data client (#3131)

* Backend overhaul fetch media by filename (#3081)

* Added class MwParseResponse and MwParseResult for receiving parse output

* Migrated fetchMediaByFilename to retrofit

* Removed unused code

* Added tests

* Migrated isUserBlockedFromCommons to retrofit (#3085)

* Switched wikimedia-android-data-client for new features

* Added UserCLient and UserInterface

* Migrated isUserBlockedFromCommons to retrofit

* Added tests and removed dead code

* Implemented ban checking functionality in UploadActivity

* Removed unused class AuthenticatedActivity

* Fixed tests

* Fixed NullPointerException when a user accessed image details without logging in.

* * Login the login token way, handle LoginResult minutely, add account based on LoginResult (#3151)

* Added progress updater in UploadService to show upload progress in no… (#3156)

* added progress updater in UploadService to show upload progress in notification

* formatting changes

* [Dependency: Quadtree] Remove unused code from cache controller (#3163)

* Basic logging with redacted sensitive headers (#3159)

* As per #3026, removed the obsolete classes in package mwapi (#3150)

* fixed compile time error (#3165)

* [Dependency fluent]: Remove unused dependency fluent (#3164)

* Donot init quiz checker in onResume (#3167)

* clear image cache on logout (#3168)

* Fixed default locale and upload locales in descrptions (#3166)

* Fix 2FA login (#3170)

* Fix build (#3172)

* Localisation updates from https://translatewiki.net.

* Closes #3094 (#3095)

* BugFix in SpinnerDescriptionsAdapter and SpinnerLanguagesAdapter (use the langguage code provided by the spinner, donot set the language to the one returned by the locale)

* Update changelog.md

* Versioning for v2.11.0

* Localisation updates from https://translatewiki.net.

* Localisation updates from https://translatewiki.net.

* Localisation updates from https://translatewiki.net.

* Center map on location clicked in nearby list and notification card(#2060) (#2366)

* center map on location clicked in nearby (#2060)

* improved animation

* Center map on location clicked in nearby list

* removed unnecessary methods

* center map on location clicked in nearby notification card

* some minor changes

* travis build error

* resolved errors

* Tidy up PR

* removed swipe to delete (#2589)

* Localisation updates from https://translatewiki.net.

* Localisation updates from https://translatewiki.net.

* Localisation updates from https://translatewiki.net.

* Localisation updates from https://translatewiki.net.

* Localisation updates from https://translatewiki.net.

* Localisation updates from https://translatewiki.net.

* edited CREDITS file (#3145)

* Unused dependencies are removed (#3141)

Part of #3128

* Localisation updates from https://translatewiki.net.

* Localisation updates from https://translatewiki.net.

* Localisation updates from https://translatewiki.net.

* Working with #3129 issue (#3146)

* Replace Hard-Coded strings with those from strings.xml.

* Localisation updates from https://translatewiki.net.

* Localisation updates from https://translatewiki.net.

* Localisation updates from https://translatewiki.net.

* Localisation updates from https://translatewiki.net.

* delete res/values-en-gb (#3153)

* Localisation updates from https://translatewiki.net.

* Localisation updates from https://translatewiki.net.

* First commit (#3093)

Fixes info icon color is Review

* Solution #3126 (#3155)

Fixed wrong upload dates after image upload

* Bugfix/fix upload presenter tests (#3158)

* Revert "Merge branch 'backend-overhaul' into master"

This reverts commit 0090f24257, reversing
changes made to 9bccbfe443.

* * Fixed upload presenter tests
* Deleted app/src/main/res/values-en-gb/error.xml

* Localisation updates from https://translatewiki.net.

* Remove dependency on Exif parsing library. (#2947)

* Remove dependency on Exif parsing library.

* Fix test.

* Localisation updates from https://translatewiki.net.

* Localisation updates from https://translatewiki.net.

* Refactor nearby classes mvp (#2969)

* Create parent contract

* Create map child contract and fill methods

* Add javadocs and specific interfaces for list

* Move general method to parent and add javadocs for parent

* Add explanation for keeping an emty View interface under NearbyListContract

* Move constracts under contract package

* Create presenters for map and list and implement user actions accordingly

* Add javadocs

* Add presenter, contract and fragment for parent Fragment of both NearyListFragment and NearbyMapFragment

* Implement missing methods

* Fix typo

* Add main views on fragment

* İmplement child fragment logic and their retain

* Relate parent presenter with parent fragment

* Add all location permission related methods to view contract and implement in fragment. Call them from presenter by passing locationServiceManager parameter

* Define refreshView method as updateMapAndList which is a better naming. Define it at presenter part.

* Define a presenter variable in fragment and call updateMapAndList method from there, if permissions are okay

* Add lock neabry method to unlisten nearby operations during updates

* Add network connection established check on view side, check it from presenter

* try to simplify previous method during refactor

* Add missing methods for NearbyMap

* Connect child fragment and prent fragment with presenter

* Change nearby design, first create views then register listeners

* AddnetworkBroadcatsReceiver on view side, call it from presenter

* Add comments

* Change the old NearbyFragment by our new NearbyParentFragment for first tests

* Prevent crash caused child fragment is actually null by checking if it is attached or not.

* Makes sure that initialize nearby operations method is called just after all views and fragments are ready and attached

* Make sure updateMAoAndList method is called when everything ready

* Rename a method with prepoer name

* Call update map and add required markers

* Find out zoom level problem

* Implement add nearby markers and add current marker methods

* remove unneeded codes copied from previous implementation

* Revert "remove unneeded codes copied from previous implementation"

This reverts commit 42539651de.

* add some commits and clear code

* Remove location listener implementation from view, handle them in presenter instead. And add timber debug notes and required method calls

* Style ,issues

* Refactor a variable name to camel case and bind search this area view

* Search this area button action is added

* Mostly implement search this area methods, not tested yet even once

* Make sure everything is called in required order and seach this area method basically works

* Rename methods accordingly and add Javadocs

* Add current location marker and remove circle around it

* Remove unused methods

* Add current location marker with object animator and remove previous marker is exists

* include clear map into add markers method and reorder methods

* Make search this area button appear at correct time

* Try to load un search this area is called

* Clear logs

* Add changes for permission made by Vivek to newly added fragments along with our nearby classes

* Add a view to nearby map ragment and insert map view as an item inside it

* Add logs to uınderstand why on meap ready callback is never called

* Add list item clicked and bottom sheet for list of nearby items is expanded

* Make nearby map ready callback came

* Add required methods to be called after map view is ready

* State: Map ready call is not called but permissions and methods are in correct order

* Remove unused logs

* Try to use SupportMapFragment instead, still no success...

* use SupportMapFragment instead

* Remove unused Near

* Upgrade mapbox sdk versions

* Remove Style import from fragment/NearbyMapFragment

* Remove Style import from **fragments/NearbyParentFragment

* Remove nearby/NearbyMapFragment

* Remove unused/old NearbyFragment and NearbyMapFragment

* Remove import of already removed class

* Make sure you removed everything related with mapbox implementation

* Update mapbox map

* Remove unused classes, do not forget centerMapToPlace doesn't work on this branch

* Remove Style class it required updated version of mapbox map

* Add base codes for new activity

* Prove that our mapbox sdk let us implement the map directly inside the activity

* Add base codes for testing mapbox activity in a single fragment inside activity

* Add codes from mapbox demo repository to test support fragment, map works in a single layer fragment

* Add base codes for test layered fragment activity

* Add Support fragment inside a fragment and proves that layered fragmentw with map works

* Test view pager and tab layout with support map fragment to see it works, it works!

* Move Contributions Fragment related codes to test activity

* Move nearby card methods

* Inject location manager and implement NerabyParentFragmentContract.View on test fragment

* Coppy the content of SupportMapFragment from mapbox repository, use this code to modify later. This method war suggested by mapbox team instead of extending the class

* Implement NearbyMapContract.View on our new SupportMapFragment

* Start to mplement logic of checking permissions

* Fix small dagger issue to inject location manager properly

* Request permission for nearby places if fragment is loaded and tab is selected

* Initialize map operations if map ready and tab is selected

* Markers loads at correct time

* Style the map according to new version of mapbox map

* Add some map elements like FABs and give their actions

* Implement map marker click actions

* Implement nearby Fabs logic with a small issue

* fix FABs are not closing problem

* Unkown problem occurs at map load when I try to use MainActivity again.

* Revert "Unkown problem occurs at map load when I try to use MainActivity again."

This reverts commit 3dc084415b.

* Search this area buttons are added but button function does not work and button visibility is problematic

* Fix issue with MainActivity with the help of Ashish

* Fix search this area button visibility issue

* Fix the issues with updating search nearby markers and camera position together

* Fix progress bar visibility issue

* Prevent loding map each time tab selected

* Take toolbar back

* Implement back button with presenter

* Add click actoion to bottom sheet details

* Add nearby list into bottom sheeet

* Make reuse existing fragments if there is any

* Code cleanup

* Cleanup

* Code cleanup

* Add lifecyle codes to prevent leaks

* Cleanup

* Code cleanup

* cleanup

* Make list item clicked and map focus to same place

* Add bookmark from list fragment

* Make nearby card click action work

* Revert "Fix conflicts"

This reverts commit f3451745d3, reversing
changes made to c5d4d5533d.

* Code cleanup

* Cleanup

* Make recenter button work when list sheet is expanded

* Cleanup

* Code cleanups

* NPE issue is not detected for now, can be solved on seperate PR

* Update map after Wikidata edit

* Cherry picked previously reverted merge, hoping this will fix enourmus amounts of diff files

* Previous merge bringed an file which should be deleted. Delete it.

* Revert irrelevant changes on build.gradle

* Revert irrelevant changes

* Jetbrains annotation is not included to build gradle, this issue caused build issues on my branch so I removed it to be able to build

* Rename ListView

* Use a singleton to access the presenter, instead of passing it to a variable inside fragment

* Fix code style issues

* Move hardcoded colors to colors.xml file

* Make larger methods smaller

* Make current location marker follow

* Do not track users position if user is searching around

* Revert irrelevant shanges at build gradle

* Remove unneeded variable

* Remove mvp directory, add sub directories directly under nearby directory instead

* Remove unneeded public definiton

* Remove unneeded namespace

* Add public defiiton, it is needed to reach the constructor.

* On Logout, fetch the CSRF token and then make the post logout call (#3182)

* Api call for logout

* call clear cached onCompleteSessionLogout

* Correction is passed file name to checkPageExistsUsingTitle, function expects file name prefixed with File: (#3194)

* Merge master (#3196)

* Localisation updates from https://translatewiki.net.

* Localisation updates from https://translatewiki.net.

* Localisation updates from https://translatewiki.net.

* Mapped values-en-gb to values-en-rGB (#3161)

Fixes "Error: Invalid resource directory name" bug.

* Localisation updates from https://translatewiki.net.

* Removes the "Other" deletion option mentioned in #3174 (#3183)

* Localisation updates from https://translatewiki.net.

* Renamed resource file to prevent build from failing (#3189)

* Localisation updates from https://translatewiki.net.

* Removed some jargon/slang from strings.xml (#3162)

* Localisation updates from https://translatewiki.net.

* solve issue Avoid 'form movements' in the login screen #1107 (#2936)

* Disabled review buttons while an image is being loaded (#3185)

* Disabled review buttons while an image is being loaded

* Added javadocs for the new methods

* Fix build
This commit is contained in:
Vivek Maskara 2019-11-06 18:05:12 +05:30 committed by Ashish Kumar
parent 2ed32162b7
commit 8ccad2277d
119 changed files with 2558 additions and 5016 deletions

View file

@ -1,7 +1,6 @@
plugins {
id 'com.github.triplet.play' version '2.2.1' apply false
}
apply from: '../gitutils.gradle'
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
@ -20,15 +19,18 @@ dependencies {
// Utils
implementation 'com.github.nicolas-raoul:Quadtree:ac16ea8035bf07'
implementation 'in.yuvi:http.fluent:1.3'
implementation 'com.squareup.okhttp3:okhttp:3.12.1'
implementation 'com.squareup.okio:okio:1.15.0'
implementation 'com.google.code.gson:gson:2.8.5'
implementation 'com.squareup.okhttp3:okhttp:4.2.0'
implementation 'com.squareup.okio:okio:2.2.2'
implementation 'io.reactivex.rxjava2:rxandroid:2.1.0'
implementation 'io.reactivex.rxjava2:rxjava:2.2.3'
implementation 'com.jakewharton.rxbinding2:rxbinding:2.1.1'
implementation 'com.jakewharton.rxbinding2:rxbinding-support-v4:2.1.1'
implementation 'com.jakewharton.rxbinding2:rxbinding-appcompat-v7:2.1.1'
implementation 'com.jakewharton.rxbinding2:rxbinding-design:2.1.1'
implementation 'com.facebook.fresco:fresco:1.13.0'
implementation 'com.dmitrybrant:wikimedia-android-data-client:0.0.18'
implementation 'org.apache.commons:commons-lang3:3.8.1'
implementation 'com.github.maskaravivek:wikimedia-android-data-client:v0.0.28'
// UI
implementation 'fr.avianey.com.viewpagerindicator:library:2.4.1.1@aar'
@ -49,6 +51,7 @@ dependencies {
api('com.github.tony19:logback-android-classic:1.1.1-6') {
exclude group: 'com.google.android', module: 'android'
}
implementation "com.squareup.okhttp3:logging-interceptor:4.2.0"
// Dependency injector
implementation "com.google.dagger:dagger-android-support:$DAGGER_VERSION"
@ -62,21 +65,21 @@ dependencies {
testImplementation 'org.robolectric:robolectric:4.3'
testImplementation 'androidx.test:core:1.2.0'
testImplementation 'com.nhaarman:mockito-kotlin:1.5.0'
testImplementation 'com.squareup.okhttp3:mockwebserver:3.10.0'
testImplementation 'com.squareup.okhttp3:mockwebserver:3.12.1'
testImplementation "org.powermock:powermock-module-junit4:2.0.0-beta.5"
testImplementation "org.powermock:powermock-api-mockito2:2.0.0-beta.5"
// Android testing
androidTestImplementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$KOTLIN_VERSION"
androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-intents:3.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.1.1'
androidTestImplementation 'androidx.test:runner:1.1.1'
androidTestImplementation 'androidx.test:rules:1.1.1'
androidTestImplementation 'androidx.annotation:annotation:1.0.2'
androidTestImplementation 'com.squareup.okhttp3:mockwebserver:3.10.0'
androidTestImplementation 'org.mockito:mockito-core:2.10.0'
androidTestUtil 'androidx.test:orchestrator:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
androidTestImplementation 'androidx.test.espresso:espresso-intents:3.2.0'
androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.2.0'
androidTestImplementation 'androidx.test:runner:1.2.0'
androidTestImplementation 'androidx.test:rules:1.2.0'
androidTestImplementation 'androidx.annotation:annotation:1.1.0'
androidTestImplementation 'com.squareup.okhttp3:mockwebserver:3.12.1'
androidTestImplementation 'org.mockito:mockito-core:2.13.0'
androidTestUtil 'androidx.test:orchestrator:1.2.0'
// Debugging
debugImplementation "com.squareup.leakcanary:leakcanary-android:$LEAK_CANARY_VERSION"
@ -187,6 +190,7 @@ android {
buildConfigField "String", "IMAGE_URL_BASE", "\"https://upload.wikimedia.org/wikipedia/commons\""
buildConfigField "String", "HOME_URL", "\"https://commons.wikimedia.org/wiki/\""
buildConfigField "String", "COMMONS_URL", "\"https://commons.wikimedia.org\""
buildConfigField "String", "WIKIDATA_URL", "\"https://wikidata.org\""
buildConfigField "String", "MOBILE_HOME_URL", "\"https://commons.m.wikimedia.org/wiki/\""
buildConfigField "String", "SIGNUP_LANDING_URL", "\"https://commons.m.wikimedia.org/w/index.php?title=Special:CreateAccount&returnto=Main+Page&returntoquery=welcome%3Dyes\""
buildConfigField "String", "SIGNUP_SUCCESS_REDIRECTION_URL", "\"https://commons.m.wikimedia.org/w/index.php?title=Main_Page&welcome=yes\""
@ -218,6 +222,7 @@ android {
buildConfigField "String", "IMAGE_URL_BASE", "\"https://upload.beta.wmflabs.org/wikipedia/commons\""
buildConfigField "String", "HOME_URL", "\"https://commons.wikimedia.beta.wmflabs.org/wiki/\""
buildConfigField "String", "COMMONS_URL", "\"https://commons.wikimedia.beta.wmflabs.org\""
buildConfigField "String", "WIKIDATA_URL", "\"https://wikidata.org\""
buildConfigField "String", "MOBILE_HOME_URL", "\"https://commons.m.wikimedia.beta.wmflabs.org/wiki/\""
buildConfigField "String", "SIGNUP_LANDING_URL", "\"https://commons.m.wikimedia.beta.wmflabs.org/w/index.php?title=Special:CreateAccount&returnto=Main+Page&returntoquery=welcome%3Dyes\""
buildConfigField "String", "SIGNUP_SUCCESS_REDIRECTION_URL", "\"https://commons.m.wikimedia.beta.wmflabs.org/w/index.php?title=Main_Page&welcome=yes\""

View file

@ -152,7 +152,8 @@
</service>
<service
android:name=".contributions.ContributionsSyncService"
android:exported="true">
android:exported="true"
android:process=":sync">
<intent-filter>
<action android:name="android.content.SyncAdapter" />
</intent-filter>
@ -161,17 +162,6 @@
android:name="android.content.SyncAdapter"
android:resource="@xml/contributions_sync_adapter" />
</service>
<service
android:name=".modifications.ModificationsSyncService"
android:exported="true">
<intent-filter>
<action android:name="android.content.SyncAdapter" />
</intent-filter>
<meta-data
android:name="android.content.SyncAdapter"
android:resource="@xml/modifications_sync_adapter" />
</service>
<service
android:name="org.acra.sender.SenderService"
@ -193,12 +183,7 @@
android:exported="false"
android:label="@string/provider_contributions"
android:syncable="true" />
<provider
android:name=".modifications.ModificationsContentProvider"
android:authorities="${applicationId}.modifications.contentprovider"
android:exported="false"
android:label="@string/provider_modifications"
android:syncable="true" />
<provider
android:name=".category.CategoryContentProvider"
android:authorities="${applicationId}.categories.contentprovider"

View file

@ -61,7 +61,7 @@ public class CommonsAppAdapter extends AppAdapter {
@Override
public void updateAccount(@NonNull LoginResult result) {
// TODO: sessionManager.updateAccount(result);
sessionManager.updateAccount(result);
}
@Override
@ -69,7 +69,8 @@ public class CommonsAppAdapter extends AppAdapter {
if (!preferences.contains(COOKIE_STORE_NAME)) {
return null;
}
return GsonUnmarshaller.unmarshal(SharedPreferenceCookieManager.class, preferences.getString(COOKIE_STORE_NAME, null));
return GsonUnmarshaller.unmarshal(SharedPreferenceCookieManager.class,
preferences.getString(COOKIE_STORE_NAME, null));
}
@Override

View file

@ -1,5 +1,12 @@
package fr.free.nrw.commons;
import static org.acra.ReportField.ANDROID_VERSION;
import static org.acra.ReportField.APP_VERSION_CODE;
import static org.acra.ReportField.APP_VERSION_NAME;
import static org.acra.ReportField.PHONE_MODEL;
import static org.acra.ReportField.STACK_TRACE;
import static org.acra.ReportField.USER_COMMENT;
import android.annotation.SuppressLint;
import android.app.Application;
import android.app.NotificationChannel;
@ -9,26 +16,12 @@ import android.database.sqlite.SQLiteDatabase;
import android.os.Build;
import android.os.Process;
import android.util.Log;
import androidx.annotation.NonNull;
import com.facebook.drawee.backends.pipeline.Fresco;
import com.facebook.imagepipeline.core.ImagePipeline;
import com.facebook.imagepipeline.core.ImagePipelineConfig;
import com.squareup.leakcanary.LeakCanary;
import com.squareup.leakcanary.RefWatcher;
import org.acra.ACRA;
import org.acra.annotation.AcraCore;
import org.acra.annotation.AcraDialog;
import org.acra.annotation.AcraMailSender;
import org.acra.data.StringFormat;
import org.wikipedia.AppAdapter;
import org.wikipedia.language.AppLanguageLookUpTable;
import java.io.File;
import javax.inject.Inject;
import javax.inject.Named;
import androidx.annotation.NonNull;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao;
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesDao;
@ -41,22 +34,24 @@ import fr.free.nrw.commons.di.ApplicationlessInjection;
import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.logging.FileLoggingTree;
import fr.free.nrw.commons.logging.LogUtils;
import fr.free.nrw.commons.modifications.ModifierSequenceDao;
import fr.free.nrw.commons.upload.FileUtils;
import fr.free.nrw.commons.utils.ConfigUtils;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.internal.functions.Functions;
import io.reactivex.plugins.RxJavaPlugins;
import io.reactivex.schedulers.Schedulers;
import java.io.File;
import javax.inject.Inject;
import javax.inject.Named;
import org.acra.ACRA;
import org.acra.annotation.AcraCore;
import org.acra.annotation.AcraDialog;
import org.acra.annotation.AcraMailSender;
import org.acra.data.StringFormat;
import org.wikipedia.AppAdapter;
import org.wikipedia.language.AppLanguageLookUpTable;
import timber.log.Timber;
import static org.acra.ReportField.ANDROID_VERSION;
import static org.acra.ReportField.APP_VERSION_CODE;
import static org.acra.ReportField.APP_VERSION_NAME;
import static org.acra.ReportField.PHONE_MODEL;
import static org.acra.ReportField.STACK_TRACE;
import static org.acra.ReportField.USER_COMMENT;
@AcraCore(
buildConfigClass = BuildConfig.class,
resReportSendSuccessToast = R.string.crash_dialog_ok_toast,
@ -250,6 +245,7 @@ public class CommonsApplication extends Application {
.observeOn(AndroidSchedulers.mainThread())
.subscribe(() -> {
Timber.d("All accounts have been removed");
clearImageCache();
//TODO: fix preference manager
defaultPrefs.clearAll();
defaultPrefs.putBoolean("firstrun", false);
@ -258,6 +254,14 @@ public class CommonsApplication extends Application {
});
}
/**
* Clear all images cache held by Fresco
*/
private void clearImageCache() {
ImagePipeline imagePipeline = Fresco.getImagePipeline();
imagePipeline.clearCaches();
}
/**
* Deletes all tables and re-creates them.
*/
@ -265,7 +269,6 @@ public class CommonsApplication extends Application {
dbOpenHelper.getReadableDatabase().close();
SQLiteDatabase db = dbOpenHelper.getWritableDatabase();
ModifierSequenceDao.Table.onDelete(db);
CategoryDao.Table.onDelete(db);
ContributionDao.Table.onDelete(db);
BookmarkPicturesDao.Table.onDelete(db);

View file

@ -4,6 +4,9 @@ import android.net.Uri;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.apache.commons.lang3.StringUtils;
import org.wikipedia.dataclient.mwapi.MwQueryPage;
import org.wikipedia.gallery.ExtMetadata;
@ -20,14 +23,13 @@ import java.util.List;
import java.util.Locale;
import java.util.Map;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.utils.CommonsDateUtil;
import fr.free.nrw.commons.utils.MediaDataExtractorUtil;
public class Media implements Parcelable {
public static final Media EMPTY = new Media("");
public static Creator<Media> CREATOR = new Creator<Media>() {
@Override
public Media createFromParcel(Parcel parcel) {
@ -156,9 +158,9 @@ public class Media implements Parcelable {
page.title(),
"",
0,
safeParseDate(metadata.dateTimeOriginal().value()),
safeParseDate(metadata.dateTime().value()),
StringUtil.fromHtml(metadata.artist().value()).toString()
safeParseDate(metadata.dateTime()),
safeParseDate(metadata.dateTime()),
StringUtil.fromHtml(metadata.artist()).toString()
);
if (!StringUtils.isBlank(imageInfo.getThumbUrl())) {
@ -170,17 +172,17 @@ public class Media implements Parcelable {
language = "default";
}
media.setDescriptions(Collections.singletonMap(language, metadata.imageDescription().value()));
media.setCategories(MediaDataExtractorUtil.extractCategoriesFromList(metadata.categories().value()));
String latitude = metadata.gpsLatitude().value();
String longitude = metadata.gpsLongitude().value();
media.setDescriptions(Collections.singletonMap(language, metadata.imageDescription()));
media.setCategories(MediaDataExtractorUtil.extractCategoriesFromList(metadata.getCategories()));
String latitude = metadata.getGpsLatitude();
String longitude = metadata.getGpsLongitude();
if (!StringUtils.isBlank(latitude) && !StringUtils.isBlank(longitude)) {
LatLng latLng = new LatLng(Double.parseDouble(latitude), Double.parseDouble(longitude), 0);
media.setCoordinates(latLng);
}
media.setLicenseInformation(metadata.licenseShortName().value(), metadata.licenseUrl().value());
media.setLicenseInformation(metadata.licenseShortName(), metadata.licenseUrl());
return media;
}

View file

@ -1,10 +1,11 @@
package fr.free.nrw.commons;
import androidx.core.text.HtmlCompat;
import javax.inject.Inject;
import javax.inject.Singleton;
import androidx.core.text.HtmlCompat;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.media.MediaClient;
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
import io.reactivex.Single;
import timber.log.Timber;
@ -17,14 +18,14 @@ import timber.log.Timber;
*/
@Singleton
public class MediaDataExtractor {
private final MediaWikiApi mediaWikiApi;
private final OkHttpJsonApiClient okHttpJsonApiClient;
private final MediaClient mediaClient;
@Inject
public MediaDataExtractor(MediaWikiApi mwApi,
OkHttpJsonApiClient okHttpJsonApiClient) {
public MediaDataExtractor(OkHttpJsonApiClient okHttpJsonApiClient,
MediaClient mediaClient) {
this.okHttpJsonApiClient = okHttpJsonApiClient;
this.mediaWikiApi = mwApi;
this.mediaClient = mediaClient;
}
/**
@ -35,7 +36,7 @@ public class MediaDataExtractor {
*/
public Single<Media> fetchMediaDetails(String filename) {
Single<Media> mediaSingle = getMediaFromFileName(filename);
Single<Boolean> pageExistsSingle = mediaWikiApi.pageExists("Commons:Deletion_requests/" + filename);
Single<Boolean> pageExistsSingle = mediaClient.checkPageExistsUsingTitle("Commons:Deletion_requests/" + filename);
Single<String> discussionSingle = getDiscussion(filename);
return Single.zip(mediaSingle, pageExistsSingle, discussionSingle, (media, deletionStatus, discussion) -> {
media.setDiscussion(discussion);
@ -52,7 +53,7 @@ public class MediaDataExtractor {
* @return return data rich Media object
*/
public Single<Media> getMediaFromFileName(String filename) {
return okHttpJsonApiClient.getMedia(filename, false);
return mediaClient.getMedia(filename);
}
/**
@ -61,8 +62,7 @@ public class MediaDataExtractor {
* @return
*/
private Single<String> getDiscussion(String filename) {
return mediaWikiApi.fetchMediaByFilename(filename.replace("File", "File talk"))
.flatMap(mediaResult -> mediaWikiApi.parseWikicode(mediaResult.getWikiSource()))
return mediaClient.getPageHtml(filename.replace("File", "File talk"))
.map(discussion -> HtmlCompat.fromHtml(discussion, HtmlCompat.FROM_HTML_MODE_LEGACY).toString())
.onErrorReturn(throwable -> {
Timber.e(throwable, "Error occurred while fetching discussion");

View file

@ -31,12 +31,22 @@ public final class OkHttpConnectionFactory {
return new OkHttpClient.Builder()
.cookieJar(SharedPreferenceCookieManager.getInstance())
.cache(NET_CACHE)
.addInterceptor(new HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY))
.addInterceptor(getLoggingInterceptor())
.addInterceptor(new UnsuccessfulResponseInterceptor())
.addInterceptor(new CommonHeaderRequestInterceptor())
.build();
}
private static HttpLoggingInterceptor getLoggingInterceptor() {
HttpLoggingInterceptor httpLoggingInterceptor = new HttpLoggingInterceptor()
.setLevel(HttpLoggingInterceptor.Level.BASIC);
httpLoggingInterceptor.redactHeader("Authorization");
httpLoggingInterceptor.redactHeader("Cookie");
return httpLoggingInterceptor;
}
private static class CommonHeaderRequestInterceptor implements Interceptor {
@Override @NonNull public Response intercept(@NonNull Chain chain) throws IOException {
Request request = chain.request().newBuilder()

View file

@ -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);
}
}
}

View file

@ -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);
}

View file

@ -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);
}
}
}

View file

@ -3,8 +3,8 @@ package fr.free.nrw.commons.auth;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.content.Context;
import androidx.annotation.Nullable;
import androidx.annotation.Nullable;
import fr.free.nrw.commons.BuildConfig;
import timber.log.Timber;
@ -12,10 +12,8 @@ public class AccountUtil {
public static final String AUTH_COOKIE = "authCookie";
public static final String AUTH_TOKEN_TYPE = "CommonsAndroid";
private final Context context;
public AccountUtil(Context context) {
this.context = context;
public AccountUtil() {
}
/**
@ -49,5 +47,4 @@ public class AccountUtil {
private static AccountManager accountManager(Context context) {
return AccountManager.get(context);
}
}

View file

@ -1,69 +0,0 @@
package fr.free.nrw.commons.auth;
import android.os.Bundle;
import javax.inject.Inject;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.theme.NavigationBaseActivity;
import fr.free.nrw.commons.utils.ViewUtil;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
import static fr.free.nrw.commons.auth.AccountUtil.AUTH_COOKIE;
public abstract class AuthenticatedActivity extends NavigationBaseActivity {
@Inject
protected SessionManager sessionManager;
@Inject
MediaWikiApi mediaWikiApi;
private String authCookie;
protected void requestAuthToken() {
if (authCookie != null) {
onAuthCookieAcquired(authCookie);
return;
}
authCookie = sessionManager.getAuthCookie();
if (authCookie != null) {
onAuthCookieAcquired(authCookie);
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (savedInstanceState != null) {
authCookie = savedInstanceState.getString(AUTH_COOKIE);
}
showBlockStatus();
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString(AUTH_COOKIE, authCookie);
}
protected abstract void onAuthCookieAcquired(String authCookie);
protected abstract void onAuthFailure();
/**
* Makes API call to check if user is blocked from Commons. If the user is blocked, a snackbar
* is created to notify the user
*/
protected void showBlockStatus() {
compositeDisposable.add(Observable.fromCallable(() -> mediaWikiApi.isUserBlockedFromCommons())
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.filter(result -> result)
.subscribe(result -> ViewUtil.showShortSnackbar(findViewById(android.R.id.content), R.string.block_notification)
));
}
}

View file

@ -1,9 +1,6 @@
package fr.free.nrw.commons.auth;
import android.accounts.Account;
import android.accounts.AccountAuthenticatorActivity;
import android.accounts.AccountAuthenticatorResponse;
import android.accounts.AccountManager;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.Intent;
@ -16,25 +13,33 @@ import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import com.google.android.material.textfield.TextInputLayout;
import java.io.IOException;
import java.util.Locale;
import javax.inject.Inject;
import javax.inject.Named;
import androidx.annotation.ColorRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatDelegate;
import androidx.core.app.NavUtils;
import androidx.core.content.ContextCompat;
import com.google.android.material.textfield.TextInputLayout;
import org.wikipedia.AppAdapter;
import org.wikipedia.dataclient.ServiceFactory;
import org.wikipedia.dataclient.WikiSite;
import org.wikipedia.dataclient.mwapi.MwQueryResponse;
import org.wikipedia.login.LoginClient;
import org.wikipedia.login.LoginClient.LoginCallback;
import org.wikipedia.login.LoginResult;
import javax.inject.Inject;
import javax.inject.Named;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;
@ -48,33 +53,36 @@ import fr.free.nrw.commons.contributions.MainActivity;
import fr.free.nrw.commons.di.ApplicationlessInjection;
import fr.free.nrw.commons.explore.categories.ExploreActivity;
import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.theme.NavigationBaseActivity;
import fr.free.nrw.commons.utils.ConfigUtils;
import fr.free.nrw.commons.utils.ViewUtil;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.schedulers.Schedulers;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
import timber.log.Timber;
import static android.view.KeyEvent.KEYCODE_ENTER;
import static android.view.View.VISIBLE;
import static android.view.inputmethod.EditorInfo.IME_ACTION_DONE;
import static fr.free.nrw.commons.auth.AccountUtil.AUTH_TOKEN_TYPE;
import static fr.free.nrw.commons.di.NetworkingModule.NAMED_COMMONS_WIKI_SITE;
public class LoginActivity extends AccountAuthenticatorActivity {
@Inject
MediaWikiApi mwApi;
@Inject
SessionManager sessionManager;
@Inject
@Named(NAMED_COMMONS_WIKI_SITE)
WikiSite commonsWikiSite;
@Inject
@Named("default_preferences")
JsonKvStore applicationKvStore;
@Inject
LoginClient loginClient;
@BindView(R.id.login_button)
Button loginButton;
@ -103,13 +111,7 @@ public class LoginActivity extends AccountAuthenticatorActivity {
private AppCompatDelegate delegate;
private LoginTextWatcher textWatcher = new LoginTextWatcher();
private CompositeDisposable compositeDisposable = new CompositeDisposable();
private Boolean loginCurrentlyInProgress = false;
private Boolean errorMessageShown = false;
private String resultantError;
private static final String RESULTANT_ERROR = "resultantError";
private static final String ERROR_MESSAGE_SHOWN = "errorMessageShown";
private static final String LOGGING_IN = "loggingIn";
private Call<MwQueryResponse> loginToken;
@Override
public void onCreate(Bundle savedInstanceState) {
@ -211,10 +213,8 @@ public class LoginActivity extends AccountAuthenticatorActivity {
}
if (sessionManager.getCurrentAccount() != null
&& sessionManager.isUserLoggedIn()
&& sessionManager.getCachedAuthCookie() != null) {
&& sessionManager.isUserLoggedIn()) {
applicationKvStore.putBoolean("login_skipped", false);
sessionManager.revalidateAuthToken();
startMainActivity();
}
@ -239,12 +239,14 @@ public class LoginActivity extends AccountAuthenticatorActivity {
passwordEdit.removeTextChangedListener(textWatcher);
twoFactorEdit.removeTextChangedListener(textWatcher);
delegate.onDestroy();
if(null!=loginClient) {
loginClient.cancel();
}
super.onDestroy();
}
@OnClick(R.id.login_button)
public void performLogin() {
loginCurrentlyInProgress = true;
Timber.d("Login to start!");
final String username = usernameEdit.getText().toString();
final String rawUsername = usernameEdit.getText().toString().trim();
@ -252,25 +254,67 @@ public class LoginActivity extends AccountAuthenticatorActivity {
String twoFactorCode = twoFactorEdit.getText().toString();
showLoggingProgressBar();
compositeDisposable.add(Observable.fromCallable(() -> login(username, password, twoFactorCode))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(result -> handleLogin(username, rawUsername, password, result)));
doLogin(username, password, twoFactorCode);
}
private String login(String username, String password, String twoFactorCode) {
try {
if (twoFactorCode.isEmpty()) {
return mwApi.login(username, password);
} else {
return mwApi.login(username, password, twoFactorCode);
}
} catch (IOException e) {
// Do something better!
return "NetworkFailure";
}
private void doLogin(String username, String password, String twoFactorCode) {
progressDialog.show();
loginToken = ServiceFactory.get(commonsWikiSite).getLoginToken();
loginToken.enqueue(
new Callback<MwQueryResponse>() {
@Override
public void onResponse(Call<MwQueryResponse> call,
Response<MwQueryResponse> response) {
loginClient.login(commonsWikiSite, username, password, null, twoFactorCode,
response.body().query().loginToken(), new LoginCallback() {
@Override
public void success(@NonNull LoginResult result) {
Timber.d("Login Success");
onLoginSuccess(result);
}
@Override
public void twoFactorPrompt(@NonNull Throwable caught,
@Nullable String token) {
Timber.d("Requesting 2FA prompt");
hideProgress();
askUserForTwoFactorAuth();
}
@Override
public void passwordResetPrompt(@Nullable String token) {
Timber.d("Showing password reset prompt");
hideProgress();
showPasswordResetPrompt();
}
@Override
public void error(@NonNull Throwable caught) {
Timber.e(caught);
hideProgress();
showMessageAndCancelDialog(caught.getLocalizedMessage());
}
});
}
@Override
public void onFailure(Call<MwQueryResponse> call, Throwable t) {
Timber.e(t);
showMessageAndCancelDialog(t.getLocalizedMessage());
}
});
}
private void hideProgress() {
progressDialog.dismiss();
}
private void showPasswordResetPrompt() {
showMessageAndCancelDialog(getString(R.string.you_must_reset_your_passsword));
}
/**
* This function is called when user skips the login.
* It redirects the user to Explore Activity.
@ -281,18 +325,6 @@ public class LoginActivity extends AccountAuthenticatorActivity {
finish();
}
private void handleLogin(String username, String rawUsername, String password, String result) {
Timber.d("Login done!");
if (result.equals("PASS")) {
handlePassResult(username, rawUsername, password);
} else {
loginCurrentlyInProgress = false;
errorMessageShown = true;
resultantError = result;
handleOtherResults(result);
}
}
private void showLoggingProgressBar() {
progressDialog = new ProgressDialog(this);
progressDialog.setIndeterminate(true);
@ -302,67 +334,18 @@ public class LoginActivity extends AccountAuthenticatorActivity {
progressDialog.show();
}
private void handlePassResult(String username, String rawUsername, String password) {
private void onLoginSuccess(LoginResult loginResult) {
if (!progressDialog.isShowing()) {
// no longer attached to activity!
return;
}
sessionManager.setUserLoggedIn(true);
AppAdapter.get().updateAccount(loginResult);
progressDialog.dismiss();
showSuccessAndDismissDialog();
requestAuthToken();
AccountAuthenticatorResponse response = null;
Bundle extras = getIntent().getExtras();
if (extras != null) {
Timber.d("Bundle of extras: %s", extras);
response = extras.getParcelable(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE);
if (response != null) {
Bundle authResult = new Bundle();
authResult.putString(AccountManager.KEY_ACCOUNT_NAME, username);
authResult.putString(AccountManager.KEY_ACCOUNT_TYPE, BuildConfig.ACCOUNT_TYPE);
response.onResult(authResult);
}
}
sessionManager.createAccount(response, username, rawUsername, password);
startMainActivity();
}
protected void requestAuthToken() {
AccountManager accountManager = AccountManager.get(this);
Account curAccount = sessionManager.getCurrentAccount();
if (curAccount != null) {
accountManager.setAuthToken(curAccount, AUTH_TOKEN_TYPE, mwApi.getAuthCookie());
}
}
/**
* Match known failure message codes and provide messages.
*
* @param result String
*/
private void handleOtherResults(String result) {
if (result.equals("NetworkFailure")) {
// Matches NetworkFailure which is created by the doInBackground method
showMessageAndCancelDialog(R.string.login_failed_network);
} else if (result.toLowerCase(Locale.getDefault()).contains("nosuchuser".toLowerCase()) || result.toLowerCase().contains("noname".toLowerCase())) {
// Matches nosuchuser, nosuchusershort, noname
showMessageAndCancelDialog(R.string.login_failed_wrong_credentials);
emptySensitiveEditFields();
} else if (result.toLowerCase(Locale.getDefault()).contains("wrongpassword".toLowerCase())) {
// Matches wrongpassword, wrongpasswordempty
showMessageAndCancelDialog(R.string.login_failed_wrong_credentials);
emptySensitiveEditFields();
} else if (result.toLowerCase(Locale.getDefault()).contains("throttle".toLowerCase())) {
// Matches unknown throttle error codes
showMessageAndCancelDialog(R.string.login_failed_throttled);
} else if (result.toLowerCase(Locale.getDefault()).contains("userblocked".toLowerCase())) {
// Matches login-userblocked
showMessageAndCancelDialog(R.string.login_failed_blocked);
} else if (result.equals("2FA")) {
askUserForTwoFactorAuth();
} else {
// Occurs with unhandled login failure codes
Timber.d("Login failed with reason: %s", result);
showMessageAndCancelDialog(R.string.login_failed_generic);
}
}
@Override
protected void onStart() {
super.onStart();
@ -402,34 +385,13 @@ public class LoginActivity extends AccountAuthenticatorActivity {
return getDelegate().getMenuInflater();
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putBoolean(LOGGING_IN, loginCurrentlyInProgress);
outState.putBoolean(ERROR_MESSAGE_SHOWN, errorMessageShown);
outState.putString(RESULTANT_ERROR, resultantError);
}
@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
loginCurrentlyInProgress = savedInstanceState.getBoolean(LOGGING_IN, false);
errorMessageShown = savedInstanceState.getBoolean(ERROR_MESSAGE_SHOWN, false);
if (loginCurrentlyInProgress) {
performLogin();
}
if (errorMessageShown) {
resultantError = savedInstanceState.getString(RESULTANT_ERROR);
if (resultantError != null) {
handleOtherResults(resultantError);
}
}
}
public void askUserForTwoFactorAuth() {
progressDialog.dismiss();
twoFactorContainer.setVisibility(VISIBLE);
twoFactorEdit.setVisibility(VISIBLE);
twoFactorEdit.requestFocus();
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, InputMethodManager.HIDE_IMPLICIT_ONLY);
showMessageAndCancelDialog(R.string.login_failed_2fa_needed);
}
@ -440,16 +402,18 @@ public class LoginActivity extends AccountAuthenticatorActivity {
}
}
public void showMessageAndCancelDialog(String error) {
showMessage(error, R.color.secondaryDarkColor);
if (progressDialog != null) {
progressDialog.cancel();
}
}
public void showSuccessAndDismissDialog() {
showMessage(R.string.login_success, R.color.primaryDarkColor);
progressDialog.dismiss();
}
public void emptySensitiveEditFields() {
passwordEdit.setText("");
twoFactorEdit.setText("");
}
public void startMainActivity() {
NavigationBaseActivity.startActivityWithFlags(this, MainActivity.class, Intent.FLAG_ACTIVITY_SINGLE_TOP);
finish();
@ -461,6 +425,12 @@ public class LoginActivity extends AccountAuthenticatorActivity {
errorMessageContainer.setVisibility(VISIBLE);
}
private void showMessage(String message, @ColorRes int colorResId) {
errorMessage.setText(message);
errorMessage.setTextColor(ContextCompat.getColor(this, colorResId));
errorMessageContainer.setVisibility(VISIBLE);
}
private AppCompatDelegate getDelegate() {
if (delegate == null) {
delegate = AppCompatDelegate.create(this, null);

View file

@ -0,0 +1,36 @@
package fr.free.nrw.commons.auth;
import io.reactivex.Observable;
import java.util.Objects;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import org.wikipedia.dataclient.Service;
import org.wikipedia.dataclient.ServiceFactory;
import org.wikipedia.dataclient.WikiSite;
import org.wikipedia.dataclient.mwapi.MwPostResponse;
/**
* Handler for logout
*/
@Singleton
public class LogoutClient {
private final Service service;
@Inject
public LogoutClient(@Named("commons-wikisite")
WikiSite commonsWikiSite) {
service = ServiceFactory.get(commonsWikiSite);
}
/**
* Fetches the CSRF token and uses that to post the logout api call
* @return
*/
public Observable<MwPostResponse> postLogout() {
return service.getCsrfToken().concatMap(tokenResponse -> service.postLogout(
Objects.requireNonNull(Objects.requireNonNull(tokenResponse.query()).csrfToken())));
}
}

View file

@ -1,28 +1,26 @@
package fr.free.nrw.commons.auth;
import android.accounts.Account;
import android.accounts.AccountAuthenticatorResponse;
import android.accounts.AccountManager;
import android.content.ContentResolver;
import android.content.Context;
import android.os.Bundle;
import android.os.Build;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.facebook.drawee.backends.pipeline.Fresco;
import com.facebook.imagepipeline.core.ImagePipeline;
import org.wikipedia.login.LoginResult;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import io.reactivex.Completable;
import io.reactivex.Observable;
import timber.log.Timber;
import static android.accounts.AccountManager.ERROR_CODE_REMOTE_EXCEPTION;
import static android.accounts.AccountManager.KEY_ACCOUNT_NAME;
import static android.accounts.AccountManager.KEY_ACCOUNT_TYPE;
/**
* Manage the current logged in user session.
@ -30,59 +28,52 @@ import static android.accounts.AccountManager.KEY_ACCOUNT_TYPE;
@Singleton
public class SessionManager {
private final Context context;
private final MediaWikiApi mediaWikiApi;
private Account currentAccount; // Unlike a savings account... ;-)
private JsonKvStore defaultKvStore;
private static final String KEY_RAWUSERNAME = "rawusername";
private Bundle userdata = new Bundle();
@Inject
public SessionManager(Context context,
MediaWikiApi mediaWikiApi,
@Named("default_preferences") JsonKvStore defaultKvStore) {
this.context = context;
this.mediaWikiApi = mediaWikiApi;
this.currentAccount = null;
this.defaultKvStore = defaultKvStore;
}
/**
* Creata a new account
*
* @param response
* @param username
* @param rawusername
* @param password
*/
public void createAccount(@Nullable AccountAuthenticatorResponse response,
String username, String rawusername, String password) {
Account account = new Account(username, BuildConfig.ACCOUNT_TYPE);
userdata.putString(KEY_RAWUSERNAME, rawusername);
boolean created = accountManager().addAccountExplicitly(account, password, userdata);
Timber.d("account creation " + (created ? "successful" : "failure"));
if (created) {
if (response != null) {
Bundle bundle = new Bundle();
bundle.putString(KEY_ACCOUNT_NAME, username);
bundle.putString(KEY_ACCOUNT_TYPE, BuildConfig.ACCOUNT_TYPE);
response.onResult(bundle);
}
} else {
if (response != null) {
response.onError(ERROR_CODE_REMOTE_EXCEPTION, "");
}
Timber.d("account creation failure");
private boolean createAccount(@NonNull String userName, @NonNull String password) {
Account account = getCurrentAccount();
if (account == null || TextUtils.isEmpty(account.name) || !account.name.equals(userName)) {
removeAccount();
account = new Account(userName, BuildConfig.ACCOUNT_TYPE);
return accountManager().addAccountExplicitly(account, password, null);
}
return true;
}
// FIXME: If the user turns it off, it shouldn't be auto turned back on
ContentResolver.setSyncAutomatically(account, BuildConfig.CONTRIBUTION_AUTHORITY, true); // Enable sync by default!
ContentResolver.setSyncAutomatically(account, BuildConfig.MODIFICATION_AUTHORITY, true); // Enable sync by default!
private void removeAccount() {
Account account = getCurrentAccount();
if (account != null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
accountManager().removeAccountExplicitly(account);
} else {
//noinspection deprecation
accountManager().removeAccount(account, null, null);
}
}
}
public void updateAccount(LoginResult result) {
boolean accountCreated = createAccount(result.getUserName(), result.getPassword());
if (accountCreated) {
setPassword(result.getPassword());
}
}
private void setPassword(@NonNull String password) {
Account account = getCurrentAccount();
if (account != null) {
accountManager().setPassword(account, password);
}
}
/**
@ -107,7 +98,7 @@ public class SessionManager {
}
@Nullable
public String getRawUserName() {
private String getRawUserName() {
Account account = getCurrentAccount();
return account == null ? null : accountManager().getUserData(account, KEY_RAWUSERNAME);
}
@ -127,46 +118,14 @@ public class SessionManager {
return AccountManager.get(context);
}
public Boolean revalidateAuthToken() {
AccountManager accountManager = AccountManager.get(context);
Account curAccount = getCurrentAccount();
if (curAccount == null) {
return false; // This should never happen
}
accountManager.invalidateAuthToken(BuildConfig.ACCOUNT_TYPE, null);
String authCookie = getAuthCookie();
if (authCookie == null) {
return false;
}
mediaWikiApi.setAuthCookie(authCookie);
return true;
}
public String getAuthCookie() {
if (!isUserLoggedIn()) {
Timber.e("User is not logged in");
return null;
} else {
String authCookie = getCachedAuthCookie();
if (authCookie == null) {
Timber.e("Auth cookie is null even after login");
}
return authCookie;
}
}
public String getCachedAuthCookie() {
return defaultKvStore.getString("getAuthCookie", null);
}
public boolean isUserLoggedIn() {
return defaultKvStore.getBoolean("isUserLoggedIn", false);
}
void setUserLoggedIn(boolean isLoggedIn) {
defaultKvStore.putBoolean("isUserLoggedIn", isLoggedIn);
}
public void forceLogin(Context context) {
if (context != null) {
LoginActivity.startYourself(context);
@ -184,7 +143,6 @@ public class SessionManager {
return Completable.fromObservable(Observable.fromArray(allAccounts)
.map(a -> accountManager.removeAccount(a, null, null).getResult()))
.doOnComplete(() -> {
mediaWikiApi.logout();
currentAccount = null;
});
}

View file

@ -10,6 +10,7 @@ import javax.inject.Singleton;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.bookmarks.Bookmark;
import fr.free.nrw.commons.media.MediaClient;
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
import io.reactivex.Observable;
import io.reactivex.ObservableSource;
@ -19,15 +20,14 @@ import io.reactivex.functions.Function;
@Singleton
public class BookmarkPicturesController {
private final OkHttpJsonApiClient okHttpJsonApiClient;
private final MediaClient mediaClient;
private final BookmarkPicturesDao bookmarkDao;
private List<Bookmark> currentBookmarks;
@Inject
public BookmarkPicturesController(OkHttpJsonApiClient okHttpJsonApiClient,
BookmarkPicturesDao bookmarkDao) {
this.okHttpJsonApiClient = okHttpJsonApiClient;
public BookmarkPicturesController(MediaClient mediaClient, BookmarkPicturesDao bookmarkDao) {
this.mediaClient = mediaClient;
this.bookmarkDao = bookmarkDao;
currentBookmarks = new ArrayList<>();
}
@ -47,7 +47,7 @@ public class BookmarkPicturesController {
private Observable<Media> getMediaFromBookmark(Bookmark bookmark) {
Media dummyMedia = new Media("");
return okHttpJsonApiClient.getMedia(bookmark.getMediaName(), false)
return mediaClient.getMedia(bookmark.getMediaName())
.map(media -> media == null ? dummyMedia : media)
.onErrorReturn(throwable -> dummyMedia)
.toObservable();

View file

@ -16,7 +16,6 @@ import timber.log.Timber;
@Singleton
public class CacheController {
private final GpsCategoryModel gpsCategoryModel;
private final QuadTree<List<String>> quadTree;
private double x, y;
private double xMinus, xPlus, yMinus, yPlus;
@ -24,8 +23,7 @@ public class CacheController {
private static final int EARTH_RADIUS = 6378137;
@Inject
CacheController(GpsCategoryModel gpsCategoryModel) {
this.gpsCategoryModel = gpsCategoryModel;
CacheController() {
quadTree = new QuadTree<>(-180, -90, +180, +90);
}
@ -36,17 +34,6 @@ public class CacheController {
Timber.d("X (longitude) value: %f, Y (latitude) value: %f", x, y);
}
public void cacheCategory() {
List<String> pointCatList = new ArrayList<>();
if (gpsCategoryModel.getGpsCatExists()) {
pointCatList.addAll(gpsCategoryModel.getCategoryList());
Timber.d("Categories being cached: %s", pointCatList);
} else {
Timber.d("No categories found, so no categories cached");
}
quadTree.set(x, y, pointCatList);
}
public List<String> findCategory() {
Point<List<String>>[] pointsFound;
//Convert decLatitude and decLongitude to a coordinate offset range

View file

@ -1,19 +1,21 @@
package fr.free.nrw.commons.category;
import android.text.TextUtils;
import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.upload.GpsCategoryModel;
import fr.free.nrw.commons.utils.StringSortingUtils;
import io.reactivex.Observable;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;
import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.upload.GpsCategoryModel;
import fr.free.nrw.commons.utils.StringSortingUtils;
import io.reactivex.Observable;
import timber.log.Timber;
/**
@ -22,7 +24,7 @@ import timber.log.Timber;
public class CategoriesModel{
private static final int SEARCH_CATS_LIMIT = 25;
private final MediaWikiApi mwApi;
private final CategoryClient categoryClient;
private final CategoryDao categoryDao;
private final JsonKvStore directKvStore;
@ -31,10 +33,10 @@ public class CategoriesModel{
@Inject GpsCategoryModel gpsCategoryModel;
@Inject
public CategoriesModel(MediaWikiApi mwApi,
public CategoriesModel(CategoryClient categoryClient,
CategoryDao categoryDao,
@Named("default_preferences") JsonKvStore directKvStore) {
this.mwApi = mwApi;
this.categoryClient = categoryClient;
this.categoryDao = categoryDao;
this.directKvStore = directKvStore;
this.categoriesCache = new HashMap<>();
@ -48,21 +50,8 @@ public class CategoriesModel{
*/
public Comparator<CategoryItem> sortBySimilarity(final String filter) {
Comparator<String> stringSimilarityComparator = StringSortingUtils.sortBySimilarity(filter);
return (firstItem, secondItem) -> {
//if the category is selected, it should get precedence
if (null != firstItem && firstItem.isSelected()) {
if (null != secondItem && secondItem.isSelected()) {
return stringSimilarityComparator
.compare(firstItem.getName(), secondItem.getName());
}
return -1;
}
if (null != secondItem && secondItem.isSelected()) {
return 1;
}
return stringSimilarityComparator
.compare(firstItem.getName(), secondItem.getName());
};
return (firstItem, secondItem) -> stringSimilarityComparator
.compare(firstItem.getName(), secondItem.getName());
}
/**
@ -134,8 +123,8 @@ public class CategoriesModel{
}
//otherwise, search API for matching categories
return mwApi
.allCategories(term, SEARCH_CATS_LIMIT)
return categoryClient
.searchCategoriesForPrefix(term, SEARCH_CATS_LIMIT)
.map(name -> new CategoryItem(name, false));
}
@ -198,7 +187,7 @@ public class CategoriesModel{
* @return
*/
private Observable<CategoryItem> getTitleCategories(String title) {
return mwApi.searchTitles(title, SEARCH_CATS_LIMIT)
return categoryClient.searchCategories(title, SEARCH_CATS_LIMIT)
.map(name -> new CategoryItem(name, false));
}
@ -268,38 +257,4 @@ public class CategoriesModel{
this.categoriesCache.clear();
this.selectedCategories.clear();
}
/**
* Search for categories
*/
public Observable<CategoryItem> searchCategories(String query, List<String> imageTitleList) {
if (TextUtils.isEmpty(query)) {
return gpsCategories()
.concatWith(titleCategories(imageTitleList))
.concatWith(recentCategories());
}
return mwApi
.searchCategories(query, SEARCH_CATS_LIMIT)
.map(s -> new CategoryItem(s, false));
}
/**
* Returns default categories
*/
public Observable<CategoryItem> getDefaultCategories(List<String> titleList) {
Observable<CategoryItem> directCategories = directCategories();
if (hasDirectCategories()) {
Timber.d("Image has direct Categories");
return directCategories
.concatWith(gpsCategories())
.concatWith(titleCategories(titleList))
.concatWith(recentCategories());
} else {
Timber.d("Image has no direct Categories");
return gpsCategories()
.concatWith(titleCategories(titleList))
.concatWith(recentCategories());
}
}
}

View file

@ -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:", ""));
}
}

View file

@ -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);
}
}

View file

@ -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");
}
}

View file

@ -2,20 +2,19 @@ package fr.free.nrw.commons.category;
import android.content.Context;
import android.content.Intent;
import android.database.DataSetObserver;
import android.os.Bundle;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import butterknife.ButterKnife;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.auth.AuthenticatedActivity;
import fr.free.nrw.commons.explore.SearchActivity;
import fr.free.nrw.commons.media.MediaDetailPagerFragment;
import fr.free.nrw.commons.theme.NavigationBaseActivity;
@ -28,7 +27,7 @@ import fr.free.nrw.commons.theme.NavigationBaseActivity;
*/
public class CategoryImagesActivity
extends AuthenticatedActivity
extends NavigationBaseActivity
implements FragmentManager.OnBackStackChangedListener,
MediaDetailPagerFragment.MediaDetailProvider,
AdapterView.OnItemClickListener{
@ -38,16 +37,6 @@ public class CategoryImagesActivity
private CategoryImagesListFragment categoryImagesListFragment;
private MediaDetailPagerFragment mediaDetails;
@Override
protected void onAuthCookieAcquired(String authCookie) {
}
@Override
protected void onAuthFailure() {
}
/**
* This method is called on backPressed of anyFragment in the activity.
* We are changing the icon here from back to hamburger icon.
@ -69,7 +58,6 @@ public class CategoryImagesActivity
supportFragmentManager = getSupportFragmentManager();
setCategoryImagesFragment();
supportFragmentManager.addOnBackStackChangedListener(this);
requestAuthToken();
initDrawer();
setPageTitle();
}

View file

@ -27,6 +27,7 @@ import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.explore.categories.ExploreActivity;
import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.media.MediaClient;
import fr.free.nrw.commons.utils.NetworkUtils;
import fr.free.nrw.commons.utils.ViewUtil;
import io.reactivex.android.schedulers.AndroidSchedulers;
@ -56,7 +57,7 @@ public class CategoryImagesListFragment extends DaggerFragment {
private boolean isLoading = true;
private String categoryName = null;
@Inject CategoryImageController controller;
@Inject MediaClient mediaClient;
@Inject
@Named("default_preferences")
JsonKvStore categoryKvStore;
@ -116,7 +117,7 @@ public class CategoryImagesListFragment extends DaggerFragment {
isLoading = true;
progressBar.setVisibility(VISIBLE);
compositeDisposable.add(controller.getCategoryImages(categoryName)
compositeDisposable.add(mediaClient.getMediaListFromCategory(categoryName)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
@ -222,7 +223,7 @@ public class CategoryImagesListFragment extends DaggerFragment {
}
progressBar.setVisibility(VISIBLE);
compositeDisposable.add(controller.getCategoryImages(categoryName)
compositeDisposable.add(mediaClient.getMediaListFromCategory(categoryName)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)

View file

@ -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);
}

View file

@ -4,15 +4,16 @@ package fr.free.nrw.commons.category;
import android.content.Intent;
import android.content.res.Configuration;
import android.os.Bundle;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ProgressBar;
import android.widget.TextView;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.pedrogomez.renderers.RVRendererAdapter;
import java.util.ArrayList;
@ -26,10 +27,8 @@ import butterknife.ButterKnife;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.explore.categories.SearchCategoriesAdapterFactory;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.utils.NetworkUtils;
import fr.free.nrw.commons.utils.ViewUtil;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
import timber.log.Timber;
@ -53,7 +52,7 @@ public class SubCategoryListFragment extends CommonsDaggerSupportFragment {
TextView categoriesNotFoundView;
private String categoryName = null;
@Inject MediaWikiApi mwApi;
@Inject CategoryClient categoryClient;
private RVRendererAdapter<String> categoriesAdapter;
private boolean isParentCategory = true;
@ -86,7 +85,7 @@ public class SubCategoryListFragment extends CommonsDaggerSupportFragment {
}
/**
* Checks for internet connection and then initializes the recycler view with 25 categories of the searched query
* Checks for internet connection and then initializes the recycler view with all(max 500) categories of the searched query
* Clearing categoryAdapter every time new keyword is searched so that user can see only new results
*/
public void initSubCategoryList() {
@ -96,17 +95,19 @@ public class SubCategoryListFragment extends CommonsDaggerSupportFragment {
return;
}
progressBar.setVisibility(View.VISIBLE);
if (!isParentCategory){
compositeDisposable.add(Observable.fromCallable(() -> mwApi.getSubCategoryList(categoryName))
if (isParentCategory) {
compositeDisposable.add(categoryClient.getParentCategoryList("Category:"+categoryName)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
.collect(ArrayList<String>::new, ArrayList::add)
.subscribe(this::handleSuccess, this::handleError));
}else {
compositeDisposable.add(Observable.fromCallable(() -> mwApi.getParentCategoryList(categoryName))
} else {
compositeDisposable.add(categoryClient.getSubCategoryList("Category:"+categoryName)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
.collect(ArrayList<String>::new, ArrayList::add)
.subscribe(this::handleSuccess, this::handleError));
}
}

View file

@ -92,6 +92,15 @@ public class Contribution extends Media {
this.dateCreatedSource = "";
}
public Contribution(Uri localUri, String imageUrl, String filename, String description, long dataLength,
Date dateCreated, Date dateUploaded, String creator, String editSummary, String decimalCoords, int state) {
super(localUri, imageUrl, filename, description, dataLength, dateCreated, dateUploaded, creator);
this.decimalCoords = decimalCoords;
this.editSummary = editSummary;
this.dateCreatedSource = "";
this.state=state;
}
public Contribution(Parcel in) {
super(in);
contentUri = in.readParcelable(Uri.class.getClassLoader());

View file

@ -106,7 +106,7 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder {
* @param contribution
*/
private void fetchAndDisplayThumbnail(DisplayableContribution contribution) {
String keyForLRUCache = getKeyForLRUCache(contribution.getContentUri());
String keyForLRUCache = contribution.getFilename();
String cacheUrl = thumbnailCache.get(keyForLRUCache);
if (!StringUtils.isBlank(cacheUrl)) {
imageView.setImageURI(cacheUrl);
@ -132,15 +132,6 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder {
}
/**
* Returns image key for the LRU cache, basically the id of the image, (the content uri is the ""+/id)
* @param contentUri
* @return
*/
private String getKeyForLRUCache(Uri contentUri) {
return contentUri.getLastPathSegment();
}
public void clear() {
compositeDisposable.clear();
}

View file

@ -1,9 +1,5 @@
package fr.free.nrw.commons.contributions;
import static fr.free.nrw.commons.contributions.Contribution.STATE_FAILED;
import static fr.free.nrw.commons.contributions.MainActivity.CONTRIBUTIONS_TAB_POSITION;
import static fr.free.nrw.commons.utils.LengthUtils.formatDistanceBetween;
import android.Manifest;
import android.content.ComponentName;
import android.content.Context;
@ -17,12 +13,19 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentManager.OnBackStackChangedListener;
import androidx.fragment.app.FragmentTransaction;
import java.util.ArrayList;
import javax.inject.Inject;
import javax.inject.Named;
import butterknife.BindView;
import butterknife.ButterKnife;
import fr.free.nrw.commons.HandlerService;
@ -41,7 +44,6 @@ import fr.free.nrw.commons.location.LocationServiceManager;
import fr.free.nrw.commons.location.LocationUpdateListener;
import fr.free.nrw.commons.media.MediaDetailPagerFragment;
import fr.free.nrw.commons.media.MediaDetailPagerFragment.MediaDetailProvider;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
import fr.free.nrw.commons.nearby.NearbyController;
import fr.free.nrw.commons.nearby.NearbyNotificationCardView;
@ -57,11 +59,12 @@ import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.schedulers.Schedulers;
import java.util.ArrayList;
import javax.inject.Inject;
import javax.inject.Named;
import timber.log.Timber;
import static fr.free.nrw.commons.contributions.Contribution.STATE_FAILED;
import static fr.free.nrw.commons.contributions.MainActivity.CONTRIBUTIONS_TAB_POSITION;
import static fr.free.nrw.commons.utils.LengthUtils.formatDistanceBetween;
public class ContributionsFragment
extends CommonsDaggerSupportFragment
implements
@ -72,7 +75,6 @@ public class ContributionsFragment
ICampaignsView, ContributionsContract.View {
@Inject @Named("default_preferences") JsonKvStore store;
@Inject ContributionDao contributionDao;
@Inject MediaWikiApi mediaWikiApi;
@Inject NearbyController nearbyController;
@Inject OkHttpJsonApiClient okHttpJsonApiClient;
@Inject CampaignsPresenter presenter;
@ -85,8 +87,8 @@ public class ContributionsFragment
private ContributionsListFragment contributionsListFragment;
private MediaDetailPagerFragment mediaDetailPagerFragment;
public static final String CONTRIBUTION_LIST_FRAGMENT_TAG = "ContributionListFragmentTag";
public static final String MEDIA_DETAIL_PAGER_FRAGMENT_TAG = "MediaDetailFragmentTag";
private static final String CONTRIBUTION_LIST_FRAGMENT_TAG = "ContributionListFragmentTag";
static final String MEDIA_DETAIL_PAGER_FRAGMENT_TAG = "MediaDetailFragmentTag";
@BindView(R.id.card_view_nearby) public NearbyNotificationCardView nearbyNotificationCardView;
@BindView(R.id.campaigns_view) CampaignView campaignView;
@ -257,7 +259,7 @@ public class ContributionsFragment
operations on first time fragment attached to an activity. Then they will be retained
until fragment life time ends.
*/
if (((MainActivity)getActivity()).isAuthCookieAcquired && !isFragmentAttachedBefore) {
if (!isFragmentAttachedBefore) {
onAuthCookieAcquired(((MainActivity)getActivity()).uploadServiceIntent);
isFragmentAttachedBefore = true;
@ -268,7 +270,7 @@ public class ContributionsFragment
* Replace FrameLayout with ContributionsListFragment, user will see contributions list. Creates
* new one if null.
*/
public void showContributionsListFragment() {
private void showContributionsListFragment() {
// show tabs on contribution list is visible
((MainActivity) getActivity()).showTabs();
// show nearby card view on contributions list is visible
@ -289,7 +291,7 @@ public class ContributionsFragment
* Replace FrameLayout with MediaDetailPagerFragment, user will see details of selected media.
* Creates new one if null.
*/
public void showMediaDetailPagerFragment() {
private void showMediaDetailPagerFragment() {
// hide tabs on media detail view is visible
((MainActivity)getActivity()).hideTabs();
// hide nearby card view on media detail is visible
@ -308,7 +310,7 @@ public class ContributionsFragment
* Called when onAuthCookieAcquired is called on authenticated parent activity
* @param uploadServiceIntent
*/
public void onAuthCookieAcquired(Intent uploadServiceIntent) {
void onAuthCookieAcquired(Intent uploadServiceIntent) {
// Since we call onAuthCookieAcquired method from onAttach, isAdded is still false. So don't use it
if (getActivity() != null) { // If fragment is attached to parent activity
@ -324,7 +326,7 @@ public class ContributionsFragment
* mediaDetailPagerFragment, and preserve previous state in back stack.
* Called when user selects a contribution.
*/
public void showDetail(int i) {
private void showDetail(int i) {
if (mediaDetailPagerFragment == null || !mediaDetailPagerFragment.isVisible()) {
mediaDetailPagerFragment = new MediaDetailPagerFragment();
showMediaDetailPagerFragment();

View file

@ -1,8 +1,5 @@
package fr.free.nrw.commons.contributions;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import android.content.res.Configuration;
import android.os.Bundle;
import android.view.LayoutInflater;
@ -13,21 +10,29 @@ import android.view.animation.AnimationUtils;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.RecyclerView.LayoutManager;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import javax.inject.Inject;
import javax.inject.Named;
import butterknife.BindView;
import butterknife.ButterKnife;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.contributions.ContributionsListAdapter.Callback;
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.kvstore.JsonKvStore;
import javax.inject.Inject;
import javax.inject.Named;
import fr.free.nrw.commons.wikidata.WikidataClient;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
/**
* Created by root on 01.06.2018.
@ -53,6 +58,8 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment {
@Inject @Named("default_preferences") JsonKvStore kvStore;
@Inject ContributionController controller;
@Inject
WikidataClient wikidataClient;
private Animation fab_close;
private Animation fab_open;
@ -163,6 +170,7 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment {
/**
* Responsible to set progress bar invisible and visible
*
* @param shouldShow True when contributions list should be hidden.
*/
public void showProgress(boolean shouldShow) {
@ -170,7 +178,7 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment {
}
public void showNoContributionsUI(boolean shouldShow) {
noContributionsYet.setVisibility(shouldShow?VISIBLE:GONE);
noContributionsYet.setVisibility(shouldShow ? VISIBLE : GONE);
}
public void onDataSetChanged() {

View file

@ -1,6 +1,7 @@
package fr.free.nrw.commons.contributions;
import android.accounts.Account;
import android.annotation.SuppressLint;
import android.content.AbstractThreadedSyncAdapter;
import android.content.ContentProviderClient;
import android.content.ContentValues;
@ -9,23 +10,13 @@ import android.content.SyncResult;
import android.database.Cursor;
import android.os.Bundle;
import android.os.RemoteException;
import android.text.TextUtils;
import org.wikipedia.util.DateUtil;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;
import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.di.ApplicationlessInjection;
import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.mwapi.LogEventResult;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.mwapi.UserClient;
import timber.log.Timber;
import static fr.free.nrw.commons.contributions.Contribution.STATE_COMPLETED;
@ -44,8 +35,8 @@ public class ContributionsSyncAdapter extends AbstractThreadedSyncAdapter {
// into the app, rather than the user's setting. Also see Github issue #52.
public static final int ABSOLUTE_CONTRIBUTIONS_LOAD_LIMIT = 500;
@SuppressWarnings("WeakerAccess")
@Inject MediaWikiApi mwApi;
@Inject
UserClient userClient;
@Inject
@Named("default_preferences")
JsonKvStore defaultKvStore;
@ -58,24 +49,19 @@ public class ContributionsSyncAdapter extends AbstractThreadedSyncAdapter {
if (filename == null) {
return false;
}
Cursor cursor = null;
try {
cursor = client.query(BASE_URI,
existsQuery,
existsSelection,
new String[]{filename},
""
);
try (Cursor cursor = client.query(BASE_URI,
existsQuery,
existsSelection,
new String[]{filename},
""
)) {
return cursor != null && cursor.getCount() != 0;
} catch (RemoteException e) {
throw new RuntimeException(e);
} finally {
if (cursor != null) {
cursor.close();
}
}
}
@SuppressLint("CheckResult")
@Override
public void onPerformSync(Account account, Bundle bundle, String authority,
ContentProviderClient contentProviderClient, SyncResult syncResult) {
@ -84,71 +70,20 @@ public class ContributionsSyncAdapter extends AbstractThreadedSyncAdapter {
.getApplicationContext())
.getCommonsApplicationComponent()
.inject(this);
// This code is fraught with possibilities of race conditions, but lalalalala I can't hear you!
// This code is(was?) fraught with possibilities of race conditions, but lalalalala I can't hear you!
String user = account.name;
String lastModified = defaultKvStore.getString("lastSyncTimestamp", "");
Date curTime = new Date();
LogEventResult result;
Boolean done = false;
String queryContinue = null;
ContributionDao contributionDao = new ContributionDao(() -> contentProviderClient);
while (!done) {
try {
result = mwApi.logEvents(user, lastModified, queryContinue, ABSOLUTE_CONTRIBUTIONS_LOAD_LIMIT);
} catch (IOException e) {
// There isn't really much we can do, eh?
// FIXME: Perhaps add EventLogging?
syncResult.stats.numIoExceptions += 1; // Not sure if this does anything. Shitty docs
Timber.d("Syncing failed due to %s", e);
return;
}
Timber.d("Last modified at %s", lastModified);
List<LogEventResult.LogEvent> logEvents = result.getLogEvents();
Timber.d("%d results!", logEvents.size());
ArrayList<ContentValues> imageValues = new ArrayList<>();
for (LogEventResult.LogEvent image : logEvents) {
if (image.isDeleted()) {
// means that this upload was deleted.
continue;
}
String filename = image.getFilename();
if (fileExists(contentProviderClient, filename)) {
Timber.d("Skipping %s", filename);
continue;
}
Date dateUpdated = image.getDateUpdated();
Contribution contrib = new Contribution(null, null, filename,
"", -1, dateUpdated, dateUpdated, user,
"", "");
contrib.setState(STATE_COMPLETED);
imageValues.add(contributionDao.toContentValues(contrib));
if (imageValues.size() % COMMIT_THRESHOLD == 0) {
try {
contentProviderClient.bulkInsert(BASE_URI, imageValues.toArray(EMPTY));
} catch (RemoteException e) {
throw new RuntimeException(e);
}
imageValues.clear();
}
}
if (imageValues.size() != 0) {
try {
contentProviderClient.bulkInsert(BASE_URI, imageValues.toArray(EMPTY));
} catch (RemoteException e) {
throw new RuntimeException(e);
}
}
queryContinue = result.getQueryContinue();
if (TextUtils.isEmpty(queryContinue)) {
done = true;
}
}
defaultKvStore.putString("lastSyncTimestamp", DateUtil.iso8601DateFormat(curTime));
userClient.logEvents(user)
.doOnNext(mwQueryLogEvent->Timber.d("Received image %s", mwQueryLogEvent.title() ))
.filter(mwQueryLogEvent -> !mwQueryLogEvent.isDeleted())
.filter(mwQueryLogEvent -> !fileExists(contentProviderClient, mwQueryLogEvent.title()))
.doOnNext(mwQueryLogEvent->Timber.d("Image %s passed filters", mwQueryLogEvent.title() ))
.map(image -> new Contribution(null, null, image.title(),
"", -1, image.date(), image.date(), user,
"", "", STATE_COMPLETED))
.map(contributionDao::toContentValues)
.buffer(10)
.subscribe(imageValues->contentProviderClient.bulkInsert(BASE_URI, imageValues.toArray(EMPTY)));
Timber.d("Oh hai, everyone! Look, a kitty!");
}
}

View file

@ -2,8 +2,10 @@ package fr.free.nrw.commons.contributions;
import android.annotation.SuppressLint;
import android.app.AlertDialog;
import android.content.ContentResolver;
import android.content.Intent;
import android.os.Bundle;
import android.os.PersistableBundle;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
@ -12,23 +14,24 @@ import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import com.google.android.material.tabs.TabLayout;
import java.util.List;
import javax.inject.Inject;
import androidx.annotation.Nullable;
import androidx.core.view.GravityCompat;
import androidx.drawerlayout.widget.DrawerLayout;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentPagerAdapter;
import androidx.viewpager.widget.ViewPager;
import com.google.android.material.tabs.TabLayout;
import java.util.List;
import javax.inject.Inject;
import butterknife.BindView;
import butterknife.ButterKnife;
import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.auth.AuthenticatedActivity;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.location.LocationServiceManager;
import fr.free.nrw.commons.nearby.NearbyNotificationCardView;
@ -38,15 +41,15 @@ import fr.free.nrw.commons.notification.Notification;
import fr.free.nrw.commons.notification.NotificationActivity;
import fr.free.nrw.commons.notification.NotificationController;
import fr.free.nrw.commons.quiz.QuizChecker;
import fr.free.nrw.commons.theme.NavigationBaseActivity;
import fr.free.nrw.commons.upload.UploadService;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
import timber.log.Timber;
import static android.content.ContentResolver.requestSync;
public class MainActivity extends AuthenticatedActivity implements FragmentManager.OnBackStackChangedListener {
public class MainActivity extends NavigationBaseActivity implements FragmentManager.OnBackStackChangedListener {
@BindView(R.id.tab_layout)
TabLayout tabLayout;
@ -66,13 +69,13 @@ public class MainActivity extends AuthenticatedActivity implements FragmentManag
public Intent uploadServiceIntent;
public boolean isAuthCookieAcquired = false;
public ContributionsActivityPagerAdapter contributionsActivityPagerAdapter;
public static final int CONTRIBUTIONS_TAB_POSITION = 0;
public static final int NEARBY_TAB_POSITION = 1;
public boolean isContributionsFragmentVisible = true; // False means nearby fragment is visible
public boolean onOrientationChanged;
private Menu menu;
private MenuItem notificationsMenuItem;
@ -82,9 +85,26 @@ public class MainActivity extends AuthenticatedActivity implements FragmentManag
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_contributions);
ButterKnife.bind(this);
requestAuthToken();
initDrawer();
setTitle(getString(R.string.navigation_item_home)); // Should I create a new string variable with another name instead?
initMain();
if (savedInstanceState != null ) {
onOrientationChanged = true; // Will be used in nearby fragment to determine significant update of map
//If nearby map was visible, call on Tab Selected to call all nearby operations
/*if (savedInstanceState.getInt("viewPagerCurrentItem") == 1) {
((NearbyFragment)contributionsActivityPagerAdapter.getItem(1)).onTabSelected(onOrientationChanged);
}*/
}
}
@Override
protected void onPostCreate(@Nullable Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
quizChecker.initQuizCheck(this);
}
@Override
@ -93,16 +113,15 @@ public class MainActivity extends AuthenticatedActivity implements FragmentManag
outState.putInt("viewPagerCurrentItem", viewPager.getCurrentItem());
}
@Override
protected void onAuthCookieAcquired(String authCookie) {
// Do a sync everytime we get here!
private void initMain() {
//Do not remove this, this triggers the sync service
ContentResolver.setSyncAutomatically(sessionManager.getCurrentAccount(),BuildConfig.CONTRIBUTION_AUTHORITY,true);
requestSync(sessionManager.getCurrentAccount(), BuildConfig.CONTRIBUTION_AUTHORITY, new Bundle());
uploadServiceIntent = new Intent(this, UploadService.class);
uploadServiceIntent.setAction(UploadService.ACTION_START_SERVICE);
startService(uploadServiceIntent);
addTabsAndFragments();
isAuthCookieAcquired = true;
if (contributionsActivityPagerAdapter.getItem(0) != null) {
((ContributionsFragment)contributionsActivityPagerAdapter.getItem(0)).onAuthCookieAcquired(uploadServiceIntent);
}
@ -219,14 +238,9 @@ public class MainActivity extends AuthenticatedActivity implements FragmentManag
}
}
@Override
protected void onAuthFailure() {
}
@Override
public void onBackPressed() {
DrawerLayout drawer = (DrawerLayout) findViewById(R.id.drawer_layout);
DrawerLayout drawer = findViewById(R.id.drawer_layout);
String contributionsFragmentTag = ((ContributionsActivityPagerAdapter) viewPager.getAdapter()).makeFragmentName(R.id.pager, 0);
String nearbyFragmentTag = ((ContributionsActivityPagerAdapter) viewPager.getAdapter()).makeFragmentName(R.id.pager, 1);
if (drawer.isDrawerOpen(GravityCompat.START)) {
@ -284,7 +298,7 @@ public class MainActivity extends AuthenticatedActivity implements FragmentManag
@SuppressLint("CheckResult")
private void setNotificationCount() {
compositeDisposable.add(Observable.fromCallable(() -> notificationController.getNotifications(false))
compositeDisposable.add(notificationController.getNotifications(false)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::initNotificationViews,
@ -424,7 +438,6 @@ public class MainActivity extends AuthenticatedActivity implements FragmentManag
protected void onResume() {
super.onResume();
setNotificationCount();
quizChecker.initQuizCheck(this);
}
@Override

View file

@ -9,7 +9,6 @@ import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesDao;
import fr.free.nrw.commons.category.CategoryDao;
import fr.free.nrw.commons.contributions.ContributionDao;
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao;
import fr.free.nrw.commons.modifications.ModifierSequenceDao;
public class DBOpenHelper extends SQLiteOpenHelper {
@ -27,7 +26,6 @@ public class DBOpenHelper extends SQLiteOpenHelper {
@Override
public void onCreate(SQLiteDatabase sqLiteDatabase) {
ContributionDao.Table.onCreate(sqLiteDatabase);
ModifierSequenceDao.Table.onCreate(sqLiteDatabase);
CategoryDao.Table.onCreate(sqLiteDatabase);
BookmarkPicturesDao.Table.onCreate(sqLiteDatabase);
BookmarkLocationsDao.Table.onCreate(sqLiteDatabase);
@ -37,7 +35,6 @@ public class DBOpenHelper extends SQLiteOpenHelper {
@Override
public void onUpgrade(SQLiteDatabase sqLiteDatabase, int from, int to) {
ContributionDao.Table.onUpdate(sqLiteDatabase, from, to);
ModifierSequenceDao.Table.onUpdate(sqLiteDatabase, from, to);
CategoryDao.Table.onUpdate(sqLiteDatabase, from, to);
BookmarkPicturesDao.Table.onUpdate(sqLiteDatabase, from, to);
BookmarkLocationsDao.Table.onUpdate(sqLiteDatabase, from, to);

View file

@ -11,19 +11,22 @@ import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Locale;
import java.util.concurrent.Callable;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.actions.PageEditClient;
import fr.free.nrw.commons.notification.NotificationHelper;
import fr.free.nrw.commons.review.ReviewController;
import fr.free.nrw.commons.utils.ViewUtilWrapper;
import io.reactivex.Observable;
import io.reactivex.Single;
import io.reactivex.SingleSource;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
import timber.log.Timber;
@ -35,20 +38,20 @@ import static fr.free.nrw.commons.notification.NotificationHelper.NOTIFICATION_D
*/
@Singleton
public class DeleteHelper {
private final MediaWikiApi mwApi;
private final SessionManager sessionManager;
private final NotificationHelper notificationHelper;
private final PageEditClient pageEditClient;
private final ViewUtilWrapper viewUtil;
private final String username;
@Inject
public DeleteHelper(MediaWikiApi mwApi,
SessionManager sessionManager,
NotificationHelper notificationHelper,
ViewUtilWrapper viewUtil) {
this.mwApi = mwApi;
this.sessionManager = sessionManager;
public DeleteHelper(NotificationHelper notificationHelper,
@Named("commons-page-edit") PageEditClient pageEditClient,
ViewUtilWrapper viewUtil,
@Named("username") String username) {
this.notificationHelper = notificationHelper;
this.pageEditClient = pageEditClient;
this.viewUtil = viewUtil;
this.username = username;
}
/**
@ -59,10 +62,11 @@ public class DeleteHelper {
* @return
*/
public Single<Boolean> makeDeletion(Context context, Media media, String reason) {
viewUtil.showShortToast(context, context.getString((R.string.delete_helper_make_deletion_toast), media.getDisplayTitle()));
return Single.fromCallable(() -> delete(media, reason))
.flatMap(result -> Single.fromCallable(() ->
showDeletionNotification(context, media, result)));
viewUtil.showShortToast(context, "Trying to nominate " + media.getDisplayTitle() + " for deletion");
return delete(media, reason)
.flatMapSingle(result -> Single.just(showDeletionNotification(context, media, result)))
.firstOrError();
}
/**
@ -71,14 +75,9 @@ public class DeleteHelper {
* @param reason
* @return
*/
private boolean delete(Media media, String reason) {
String editToken;
String authCookie;
private Observable<Boolean> delete(Media media, String reason) {
Timber.d("thread is delete %s", Thread.currentThread().getName());
String summary = "Nominating " + media.getFilename() + " for deletion.";
authCookie = sessionManager.getAuthCookie();
mwApi.setAuthCookie(authCookie);
Calendar calendar = Calendar.getInstance();
String fileDeleteString = "{{delete|reason=" + reason +
"|subpage=" + media.getFilename() +
@ -99,26 +98,23 @@ public class DeleteHelper {
String userPageString = "\n{{subst:idw|" + media.getFilename() +
"}} ~~~~";
try {
editToken = mwApi.getEditToken();
if(editToken == null) {
return false;
}
mwApi.prependEdit(editToken, fileDeleteString + "\n",
media.getFilename(), summary);
mwApi.edit(editToken, subpageString + "\n",
"Commons:Deletion_requests/" + media.getFilename(), summary);
mwApi.appendEdit(editToken, logPageString + "\n",
"Commons:Deletion_requests/" + date, summary);
mwApi.appendEdit(editToken, userPageString + "\n",
"User_Talk:" + media.getCreator(), summary);
} catch (Exception e) {
Timber.e(e);
return false;
}
return true;
return pageEditClient.prependEdit(media.getFilename(), fileDeleteString + "\n", summary)
.flatMap(result -> {
if (result) {
return pageEditClient.edit("Commons:Deletion_requests/" + media.getFilename(), subpageString + "\n", summary);
}
throw new RuntimeException("Failed to nominate for deletion");
}).flatMap(result -> {
if (result) {
return pageEditClient.appendEdit("Commons:Deletion_requests/" + date, logPageString + "\n", summary);
}
throw new RuntimeException("Failed to nominate for deletion");
}).flatMap(result -> {
if (result) {
return pageEditClient.appendEdit("User_Talk:" + username, userPageString + "\n", summary);
}
throw new RuntimeException("Failed to nominate for deletion");
});
}
private boolean showDeletionNotification(Context context, Media media, boolean result) {
@ -189,7 +185,12 @@ public class DeleteHelper {
}
}
makeDeletion(context, media, reason)
Timber.d("thread is askReasonAndExecute %s", Thread.currentThread().getName());
String finalReason = reason;
Single.defer((Callable<SingleSource<Boolean>>) () ->
makeDeletion(context, media, finalReason))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(aBoolean -> {

View file

@ -1,6 +1,5 @@
package fr.free.nrw.commons.di;
import fr.free.nrw.commons.contributions.ContributionsModule;
import javax.inject.Singleton;
import dagger.Component;
@ -10,8 +9,8 @@ import dagger.android.support.AndroidSupportInjectionModule;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.auth.LoginActivity;
import fr.free.nrw.commons.contributions.ContributionViewHolder;
import fr.free.nrw.commons.contributions.ContributionsModule;
import fr.free.nrw.commons.contributions.ContributionsSyncAdapter;
import fr.free.nrw.commons.modifications.ModificationsSyncAdapter;
import fr.free.nrw.commons.nearby.PlaceRenderer;
import fr.free.nrw.commons.review.ReviewController;
import fr.free.nrw.commons.settings.SettingsFragment;
@ -36,8 +35,6 @@ public interface CommonsApplicationComponent extends AndroidInjector<Application
void inject(ContributionsSyncAdapter syncAdapter);
void inject(ModificationsSyncAdapter syncAdapter);
void inject(LoginActivity activity);
void inject(SettingsFragment fragment);

View file

@ -5,24 +5,21 @@ import android.content.ContentProviderClient;
import android.content.Context;
import android.view.inputmethod.InputMethodManager;
import androidx.collection.LruCache;
import com.google.gson.Gson;
import org.wikipedia.dataclient.WikiSite;
import io.reactivex.Scheduler;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
import org.wikipedia.dataclient.WikiSite;
import org.wikipedia.AppAdapter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import javax.inject.Named;
import javax.inject.Singleton;
import androidx.collection.LruCache;
import dagger.Module;
import dagger.Provides;
import fr.free.nrw.commons.BuildConfig;
@ -37,6 +34,9 @@ import fr.free.nrw.commons.upload.UploadController;
import fr.free.nrw.commons.utils.ConfigUtils;
import fr.free.nrw.commons.wikidata.WikidataEditListener;
import fr.free.nrw.commons.wikidata.WikidataEditListenerImpl;
import io.reactivex.Scheduler;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
@Module
@SuppressWarnings({"WeakerAccess", "unused"})
@ -85,7 +85,7 @@ public class CommonsApplicationModule {
@Provides
public AccountUtil providesAccountUtil(Context context) {
return new AccountUtil(context);
return new AccountUtil();
}
@Provides
@ -188,7 +188,13 @@ public class CommonsApplicationModule {
@Named(MAIN_THREAD)
@Provides
public Scheduler providesMainThread(){
public Scheduler providesMainThread() {
return AndroidSchedulers.mainThread();
}
@Named("username")
@Provides
public String provideLoggedInUsername() {
return Objects.toString(AppAdapter.get().getUserName(), "");
}
}

View file

@ -7,7 +7,6 @@ import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesContentProvider;
import fr.free.nrw.commons.category.CategoryContentProvider;
import fr.free.nrw.commons.contributions.ContributionsContentProvider;
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesContentProvider;
import fr.free.nrw.commons.modifications.ModificationsContentProvider;
@Module
@SuppressWarnings({"WeakerAccess", "unused"})
@ -16,9 +15,6 @@ public abstract class ContentProviderBuilderModule {
@ContributesAndroidInjector
abstract ContributionsContentProvider bindContributionsContentProvider();
@ContributesAndroidInjector
abstract ModificationsContentProvider bindModificationsContentProvider();
@ContributesAndroidInjector
abstract CategoryContentProvider bindCategoryContentProvider();

View file

@ -2,11 +2,16 @@ package fr.free.nrw.commons.di;
import android.content.Context;
import androidx.annotation.NonNull;
import com.google.gson.Gson;
import org.wikipedia.csrf.CsrfTokenClient;
import org.wikipedia.dataclient.Service;
import org.wikipedia.dataclient.ServiceFactory;
import org.wikipedia.dataclient.WikiSite;
import org.wikipedia.json.GsonUtil;
import org.wikipedia.login.LoginClient;
import java.io.File;
import java.util.concurrent.TimeUnit;
@ -14,15 +19,18 @@ import java.util.concurrent.TimeUnit;
import javax.inject.Named;
import javax.inject.Singleton;
import androidx.annotation.NonNull;
import dagger.Module;
import dagger.Provides;
import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.actions.PageEditClient;
import fr.free.nrw.commons.actions.PageEditInterface;
import fr.free.nrw.commons.category.CategoryInterface;
import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.mwapi.ApacheHttpClientMediaWikiApi;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.media.MediaInterface;
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
import fr.free.nrw.commons.mwapi.UserInterface;
import fr.free.nrw.commons.review.ReviewInterface;
import fr.free.nrw.commons.upload.UploadInterface;
import okhttp3.Cache;
import okhttp3.HttpUrl;
import okhttp3.OkHttpClient;
@ -39,6 +47,12 @@ public class NetworkingModule {
public static final long OK_HTTP_CACHE_SIZE = 10 * 1024 * 1024;
public static final String NAMED_COMMONS_WIKI_SITE = "commons-wikisite";
private static final String NAMED_WIKI_DATA_WIKI_SITE = "wikidata-wikisite";
public static final String NAMED_COMMONS_CSRF = "commons-csrf";
public static final String NAMED_WIKI_DATA_CSRF = "wikidata-csrf";
@Provides
@Singleton
public OkHttpClient provideOkHttpClient(Context context,
@ -62,14 +76,6 @@ public class NetworkingModule {
return httpLoggingInterceptor;
}
@Provides
@Singleton
public MediaWikiApi provideMediaWikiApi(Context context,
@Named("default_preferences") JsonKvStore defaultKvStore,
Gson gson) {
return new ApacheHttpClientMediaWikiApi(context, BuildConfig.WIKIMEDIA_API_HOST, BuildConfig.WIKIDATA_API_HOST, defaultKvStore, gson);
}
@Provides
@Singleton
public OkHttpJsonApiClient provideOkHttpJsonApiClient(OkHttpClient okHttpClient,
@ -81,10 +87,30 @@ public class NetworkingModule {
WIKIDATA_SPARQL_QUERY_URL,
BuildConfig.WIKIMEDIA_CAMPAIGNS_URL,
BuildConfig.WIKIMEDIA_API_HOST,
defaultKvStore,
gson);
}
@Named(NAMED_COMMONS_CSRF)
@Provides
@Singleton
public CsrfTokenClient provideCommonsCsrfTokenClient(@Named(NAMED_COMMONS_WIKI_SITE) WikiSite commonsWikiSite) {
return new CsrfTokenClient(commonsWikiSite, commonsWikiSite);
}
@Named(NAMED_WIKI_DATA_CSRF)
@Provides
@Singleton
public CsrfTokenClient provideWikidataCsrfTokenClient(@Named(NAMED_COMMONS_WIKI_SITE) WikiSite commonsWikiSite,
@Named(NAMED_WIKI_DATA_WIKI_SITE) WikiSite wikidataWikiSite) {
return new CsrfTokenClient(wikidataWikiSite, commonsWikiSite);
}
@Provides
@Singleton
public LoginClient provideLoginClient() {
return new LoginClient();
}
@Provides
@Named("wikimedia_api_host")
@NonNull
@ -101,6 +127,20 @@ public class NetworkingModule {
return HttpUrl.parse(TOOLS_FORGE_URL);
}
@Provides
@Singleton
@Named(NAMED_COMMONS_WIKI_SITE)
public WikiSite provideCommonsWikiSite() {
return new WikiSite(BuildConfig.COMMONS_URL);
}
@Provides
@Singleton
@Named(NAMED_WIKI_DATA_WIKI_SITE)
public WikiSite provideWikidataWikiSite() {
return new WikiSite(BuildConfig.WIKIDATA_URL);
}
/**
* Gson objects are very heavy. The app should ideally be using just one instance of it instead of creating new instances everywhere.
* @return returns a singleton Gson instance
@ -113,14 +153,77 @@ public class NetworkingModule {
@Provides
@Singleton
@Named("commons-wikisite")
public WikiSite provideCommonsWikiSite() {
return new WikiSite(BuildConfig.COMMONS_URL);
@Named("commons-service")
public Service provideCommonsService(@Named(NAMED_COMMONS_WIKI_SITE) WikiSite commonsWikiSite) {
return ServiceFactory.get(commonsWikiSite);
}
@Provides
@Singleton
public ReviewInterface provideReviewInterface(@Named("commons-wikisite") WikiSite commonsWikiSite) {
@Named("wikidata-service")
public Service provideWikidataService(@Named(NAMED_WIKI_DATA_WIKI_SITE) WikiSite wikidataWikiSite) {
return ServiceFactory.get(wikidataWikiSite, BuildConfig.WIKIDATA_URL, Service.class);
}
@Provides
@Singleton
public ReviewInterface provideReviewInterface(@Named(NAMED_COMMONS_WIKI_SITE) WikiSite commonsWikiSite) {
return ServiceFactory.get(commonsWikiSite, BuildConfig.COMMONS_URL, ReviewInterface.class);
}
@Provides
@Singleton
public UploadInterface provideUploadInterface(@Named(NAMED_COMMONS_WIKI_SITE) WikiSite commonsWikiSite) {
return ServiceFactory.get(commonsWikiSite, BuildConfig.COMMONS_URL, UploadInterface.class);
}
@Named("commons-page-edit-service")
@Provides
@Singleton
public PageEditInterface providePageEditService(@Named(NAMED_COMMONS_WIKI_SITE) WikiSite commonsWikiSite) {
return ServiceFactory.get(commonsWikiSite, BuildConfig.COMMONS_URL, PageEditInterface.class);
}
@Named("wikidata-page-edit-service")
@Provides
@Singleton
public PageEditInterface provideWikiDataPageEditService(@Named(NAMED_WIKI_DATA_WIKI_SITE) WikiSite wikiDataWikiSite) {
return ServiceFactory.get(wikiDataWikiSite, BuildConfig.WIKIDATA_URL, PageEditInterface.class);
}
@Named("commons-page-edit")
@Provides
@Singleton
public PageEditClient provideCommonsPageEditClient(@Named(NAMED_COMMONS_CSRF) CsrfTokenClient csrfTokenClient,
@Named("commons-page-edit-service") PageEditInterface pageEditInterface,
@Named("commons-service") Service service) {
return new PageEditClient(csrfTokenClient, pageEditInterface, service);
}
@Named("wikidata-page-edit")
@Provides
@Singleton
public PageEditClient provideWikidataPageEditClient(@Named(NAMED_WIKI_DATA_CSRF) CsrfTokenClient csrfTokenClient,
@Named("wikidata-page-edit-service") PageEditInterface pageEditInterface,
@Named("wikidata-service") Service service) {
return new PageEditClient(csrfTokenClient, pageEditInterface, service);
}
@Provides
@Singleton
public MediaInterface provideMediaInterface(@Named(NAMED_COMMONS_WIKI_SITE) WikiSite commonsWikiSite) {
return ServiceFactory.get(commonsWikiSite, BuildConfig.COMMONS_URL, MediaInterface.class);
}
@Provides
@Singleton
public CategoryInterface provideCategoryInterface(@Named(NAMED_COMMONS_WIKI_SITE) WikiSite commonsWikiSite) {
return ServiceFactory.get(commonsWikiSite, BuildConfig.COMMONS_URL, CategoryInterface.class);
}
@Provides
@Singleton
public UserInterface provideUserInterface(@Named(NAMED_COMMONS_WIKI_SITE) WikiSite commonsWikiSite) {
return ServiceFactory.get(commonsWikiSite, BuildConfig.COMMONS_URL, UserInterface.class);
}
}

View file

@ -3,15 +3,16 @@ package fr.free.nrw.commons.explore.categories;
import android.content.res.Configuration;
import android.os.Bundle;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ProgressBar;
import android.widget.TextView;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.pedrogomez.renderers.RVRendererAdapter;
import java.util.ArrayList;
@ -25,15 +26,14 @@ import javax.inject.Named;
import butterknife.BindView;
import butterknife.ButterKnife;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.category.CategoryClient;
import fr.free.nrw.commons.category.CategoryDetailsActivity;
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.explore.recentsearches.RecentSearch;
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao;
import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.utils.NetworkUtils;
import fr.free.nrw.commons.utils.ViewUtil;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
import timber.log.Timber;
@ -58,9 +58,11 @@ public class SearchCategoryFragment extends CommonsDaggerSupportFragment {
String query;
@BindView(R.id.bottomProgressBar)
ProgressBar bottomProgressBar;
boolean isLoadingCategories;
@Inject RecentSearchesDao recentSearchesDao;
@Inject MediaWikiApi mwApi;
@Inject CategoryClient categoryClient;
@Inject
@Named("default_preferences")
JsonKvStore basicKvStore;
@ -135,33 +137,36 @@ public class SearchCategoryFragment extends CommonsDaggerSupportFragment {
progressBar.setVisibility(GONE);
queryList.clear();
categoriesAdapter.clear();
compositeDisposable.add(Observable.fromCallable(() -> mwApi.searchCategory(query,queryList.size()))
compositeDisposable.add(categoryClient.searchCategories(query,25)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
.doOnSubscribe(disposable -> saveQuery(query))
.collect(ArrayList<String>::new, ArrayList::add)
.subscribe(this::handleSuccess, this::handleError));
}
/**
* Adds more results to existing search results
* Adds 25 more results to existing search results
*/
public void addCategoriesToList(String query) {
if(isLoadingCategories) return;
isLoadingCategories=true;
this.query = query;
bottomProgressBar.setVisibility(View.VISIBLE);
progressBar.setVisibility(GONE);
compositeDisposable.add(Observable.fromCallable(() -> mwApi.searchCategory(query,queryList.size()))
compositeDisposable.add(categoryClient.searchCategories(query,25, queryList.size())
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
.collect(ArrayList<String>::new, ArrayList::add)
.subscribe(this::handlePaginationSuccess, this::handleError));
}
/**
* Handles the success scenario
* it initializes the recycler view by adding items to the adapter
* @param mediaList
*/
private void handlePaginationSuccess(List<String> mediaList) {
queryList.addAll(mediaList);
@ -169,6 +174,7 @@ public class SearchCategoryFragment extends CommonsDaggerSupportFragment {
bottomProgressBar.setVisibility(GONE);
categoriesAdapter.addAll(mediaList);
categoriesAdapter.notifyDataSetChanged();
isLoadingCategories=false;
}
@ -176,7 +182,6 @@ public class SearchCategoryFragment extends CommonsDaggerSupportFragment {
/**
* Handles the success scenario
* it initializes the recycler view by adding items to the adapter
* @param mediaList
*/
private void handleSuccess(List<String> mediaList) {
queryList = mediaList;
@ -194,7 +199,6 @@ public class SearchCategoryFragment extends CommonsDaggerSupportFragment {
/**
* Logs and handles API error scenario
* @param throwable
*/
private void handleError(Throwable throwable) {
Timber.e(throwable, "Error occurred while loading queried categories");
@ -213,7 +217,7 @@ public class SearchCategoryFragment extends CommonsDaggerSupportFragment {
private void initErrorView() {
progressBar.setVisibility(GONE);
categoriesNotFoundView.setVisibility(VISIBLE);
categoriesNotFoundView.setText(getString(R.string.categories_not_found, query));
categoriesNotFoundView.setText(getString(R.string.categories_not_found));
}
/**

View file

@ -24,7 +24,6 @@ import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import butterknife.BindView;
import butterknife.ButterKnife;
import com.pedrogomez.renderers.RVRendererAdapter;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
@ -32,18 +31,11 @@ import fr.free.nrw.commons.explore.SearchActivity;
import fr.free.nrw.commons.explore.recentsearches.RecentSearch;
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao;
import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
import fr.free.nrw.commons.media.MediaClient;
import fr.free.nrw.commons.utils.NetworkUtils;
import fr.free.nrw.commons.utils.ViewUtil;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
import javax.inject.Named;
import timber.log.Timber;
import static android.view.View.GONE;
@ -69,7 +61,7 @@ public class SearchImageFragment extends CommonsDaggerSupportFragment {
@Inject RecentSearchesDao recentSearchesDao;
@Inject
OkHttpJsonApiClient okHttpJsonApiClient;
MediaClient mediaClient;
@Inject
@Named("default_preferences")
JsonKvStore defaultKvStore;
@ -148,7 +140,7 @@ public class SearchImageFragment extends CommonsDaggerSupportFragment {
bottomProgressBar.setVisibility(GONE);
queryList.clear();
imagesAdapter.clear();
compositeDisposable.add(okHttpJsonApiClient.getMediaList("search", query)
compositeDisposable.add(mediaClient.getMediaListFromSearch(query)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
@ -165,7 +157,7 @@ public class SearchImageFragment extends CommonsDaggerSupportFragment {
this.query = query;
bottomProgressBar.setVisibility(View.VISIBLE);
progressBar.setVisibility(GONE);
compositeDisposable.add(okHttpJsonApiClient.getMediaList("search", query)
compositeDisposable.add(mediaClient.getMediaListFromSearch(query)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
@ -228,7 +220,7 @@ public class SearchImageFragment extends CommonsDaggerSupportFragment {
private void initErrorView() {
progressBar.setVisibility(GONE);
imagesNotFoundView.setVisibility(VISIBLE);
imagesNotFoundView.setText(getString(R.string.images_not_found, query));
imagesNotFoundView.setText(getString(R.string.images_not_found));
}
/**

View file

@ -0,0 +1,173 @@
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);
}
@NonNull
public Single<String> getPageHtml(String title){
return mediaInterface.getPageHtml(title)
.filter(MwParseResponse::success)
.map(MwParseResponse::parse)
.map(MwParseResult::text)
.first("");
}
}

View file

@ -1,9 +1,5 @@
package fr.free.nrw.commons.media;
import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE;
import static android.content.Context.DOWNLOAD_SERVICE;
import static fr.free.nrw.commons.Utils.handleWebUrl;
import android.annotation.SuppressLint;
import android.app.DownloadManager;
import android.content.Intent;
@ -18,10 +14,15 @@ import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentStatePagerAdapter;
import androidx.viewpager.widget.ViewPager;
import javax.inject.Inject;
import javax.inject.Named;
import butterknife.BindView;
import butterknife.ButterKnife;
import fr.free.nrw.commons.Media;
@ -37,18 +38,18 @@ import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.explore.SearchActivity;
import fr.free.nrw.commons.explore.categories.ExploreActivity;
import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.utils.ImageUtils;
import fr.free.nrw.commons.utils.NetworkUtils;
import fr.free.nrw.commons.utils.PermissionUtils;
import fr.free.nrw.commons.utils.ViewUtil;
import javax.inject.Inject;
import javax.inject.Named;
import timber.log.Timber;
import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE;
import static android.content.Context.DOWNLOAD_SERVICE;
import static fr.free.nrw.commons.Utils.handleWebUrl;
public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment implements ViewPager.OnPageChangeListener {
@Inject MediaWikiApi mwApi;
@Inject SessionManager sessionManager;
@Inject @Named("default_preferences") JsonKvStore store;
@Inject BookmarkPicturesDao bookmarkDao;

View file

@ -0,0 +1,87 @@
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 {
String MEDIA_PARAMS="&prop=imageinfo&iiprop=url|extmetadata&iiurlwidth=640" +
"&iiextmetadatafilter=DateTime|Categories|GPSLatitude|GPSLongitude|ImageDescription|DateTimeOriginal" +
"|Artist|LicenseShortName|LicenseUrl";
/**
* 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
MEDIA_PARAMS)
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
MEDIA_PARAMS)
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" +
MEDIA_PARAMS)
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" +
MEDIA_PARAMS)
Observable<MwQueryResponse> getMediaWithGenerator(@Query("titles") String title);
@GET("w/api.php?format=json&action=parse&prop=text")
Observable<MwParseResponse> getPageHtml(@Query("page") String title);
}

View file

@ -0,0 +1,25 @@
package fr.free.nrw.commons.media;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import org.wikipedia.dataclient.mwapi.MwResponse;
public class MwParseResponse extends MwResponse {
@Nullable
private MwParseResult parse;
@Nullable
public MwParseResult parse() {
return parse;
}
public boolean success() {
return parse != null;
}
@VisibleForTesting
protected void setParse(@Nullable MwParseResult parse) {
this.parse = parse;
}
}

View file

@ -0,0 +1,20 @@
package fr.free.nrw.commons.media;
import androidx.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
public class MwParseResult {
@SuppressWarnings("unused") private int pageid;
@SuppressWarnings("unused") private int index;
private MwParseText text;
public String text() {
return text.text;
}
public class MwParseText{
@SerializedName("*") private String text;
}
}

View file

@ -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.";
}
}

View file

@ -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;
}
}

View file

@ -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();
}
}

View file

@ -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();
}
}

View file

@ -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;
}
}

View file

@ -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);
}
}
}

View file

@ -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;
}
}

View file

@ -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) + ".";
}
}

View file

@ -1,879 +0,0 @@
package fr.free.nrw.commons.mwapi;
import android.content.Context;
import android.net.Uri;
import android.text.TextUtils;
import com.google.gson.Gson;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.conn.ClientConnectionManager;
import org.apache.http.conn.scheme.PlainSocketFactory;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.impl.client.AbstractHttpClient;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.CoreProtocolPNames;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.wikipedia.util.DateUtil;
import java.io.IOException;
import java.io.InputStream;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.Callable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.auth.AccountUtil;
import fr.free.nrw.commons.category.CategoryImageUtils;
import fr.free.nrw.commons.category.QueryContinue;
import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.notification.Notification;
import fr.free.nrw.commons.notification.NotificationUtils;
import fr.free.nrw.commons.utils.ViewUtil;
import io.reactivex.Observable;
import io.reactivex.Single;
import timber.log.Timber;
/**
* @author Addshore
*/
public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
private AbstractHttpClient httpClient;
private CustomMwApi api;
private CustomMwApi wikidataApi;
private Context context;
private JsonKvStore defaultKvStore;
private Gson gson;
private final String ERROR_CODE_BAD_TOKEN = "badtoken";
public ApacheHttpClientMediaWikiApi(Context context,
String apiURL,
String wikidatApiURL,
JsonKvStore defaultKvStore,
Gson gson) {
this.context = context;
BasicHttpParams params = new BasicHttpParams();
SchemeRegistry schemeRegistry = new SchemeRegistry();
schemeRegistry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
final SSLSocketFactory sslSocketFactory = SSLSocketFactory.getSocketFactory();
schemeRegistry.register(new Scheme("https", sslSocketFactory, 443));
ClientConnectionManager cm = new ThreadSafeClientConnManager(params, schemeRegistry);
params.setParameter(CoreProtocolPNames.USER_AGENT, CommonsApplication.getInstance().getUserAgent());
httpClient = new DefaultHttpClient(cm, params);
if (BuildConfig.DEBUG) {
httpClient.addRequestInterceptor(NetworkInterceptors.getHttpRequestInterceptor());
}
api = new CustomMwApi(apiURL, httpClient);
wikidataApi = new CustomMwApi(wikidatApiURL, httpClient);
this.defaultKvStore = defaultKvStore;
this.gson = gson;
}
/**
* @param username String
* @param password String
* @return String as returned by this.getErrorCodeToReturn()
* @throws IOException On api request IO issue
*/
public String login(String username, String password) throws IOException {
String loginToken = getLoginToken();
Timber.d("Login token is %s", loginToken);
return getErrorCodeToReturn(api.action("clientlogin")
.param("rememberMe", "1")
.param("username", username)
.param("password", password)
.param("logintoken", loginToken)
.param("loginreturnurl", "https://commons.wikimedia.org")
.post());
}
/**
* @param username String
* @param password String
* @param twoFactorCode String
* @return String as returned by this.getErrorCodeToReturn()
* @throws IOException On api request IO issue
*/
public String login(String username, String password, String twoFactorCode) throws IOException {
String loginToken = getLoginToken();
Timber.d("Login token is %s", loginToken);
return getErrorCodeToReturn(api.action("clientlogin")
.param("rememberMe", "true")
.param("username", username)
.param("password", password)
.param("logintoken", loginToken)
.param("logincontinue", "true")
.param("OATHToken", twoFactorCode)
.post());
}
private String getLoginToken() throws IOException {
return api.action("query")
.param("action", "query")
.param("meta", "tokens")
.param("type", "login")
.post()
.getString("/api/query/tokens/@logintoken");
}
/**
* @param loginCustomApiResult CustomApiResult Any clientlogin api result
* @return String On success: "PASS"
* continue: "2FA" (More information required for 2FA)
* failure: A failure message code (defined by mediawiki)
* misc: genericerror-UI, genericerror-REDIRECT, genericerror-RESTART
*/
private String getErrorCodeToReturn(CustomApiResult loginCustomApiResult) {
String status = loginCustomApiResult.getString("/api/clientlogin/@status");
if (status.equals("PASS")) {
api.isLoggedIn = true;
setAuthCookieOnLogin(true);
return status;
} else if (status.equals("FAIL")) {
setAuthCookieOnLogin(false);
return loginCustomApiResult.getString("/api/clientlogin/@messagecode");
} else if (
status.equals("UI")
&& loginCustomApiResult.getString("/api/clientlogin/requests/_v/@id").equals("TOTPAuthenticationRequest")
&& loginCustomApiResult.getString("/api/clientlogin/requests/_v/@provider").equals("Two-factor authentication (OATH).")
) {
setAuthCookieOnLogin(false);
return "2FA";
}
// UI, REDIRECT, RESTART
return "genericerror-" + status;
}
private void setAuthCookieOnLogin(boolean isLoggedIn) {
if (isLoggedIn) {
defaultKvStore.putBoolean("isUserLoggedIn", true);
defaultKvStore.putString("getAuthCookie", api.getAuthCookie());
} else {
defaultKvStore.putBoolean("isUserLoggedIn", false);
defaultKvStore.remove("getAuthCookie");
}
}
@Override
public String getAuthCookie() {
return api.getAuthCookie();
}
@Override
public void setAuthCookie(String authCookie) {
api.setAuthCookie(authCookie);
}
@Override
public boolean validateLogin() throws IOException {
boolean validateLoginResp = api.validateLogin();
Timber.d("Validate login response is %s", validateLoginResp);
return validateLoginResp;
}
@Override
public String getEditToken() throws IOException {
String editToken = api.action("query")
.param("meta", "tokens")
.post()
.getString("/api/query/tokens/@csrftoken");
Timber.d("MediaWiki edit token is %s", editToken);
return editToken;
}
@Override
public String getCentralAuthToken() throws IOException {
CustomApiResult result = api.action("centralauthtoken").get();
String centralAuthToken = result.getString("/api/centralauthtoken/@centralauthtoken");
Timber.d("MediaWiki Central auth token is %s", centralAuthToken);
if ((centralAuthToken == null || centralAuthToken.isEmpty())
&& "notLoggedIn".equals(result.getString("api/error/@code"))) {
Timber.d("Central auth token isn't valid. Trying to fetch a fresh token");
api.removeAllCookies();
String loginResultCode = login(AccountUtil.getUserName(context), AccountUtil.getPassword(context));
if (loginResultCode.equals("PASS")) {
return getCentralAuthToken();
} else if (loginResultCode.equals("2FA")) {
Timber.e("Cannot refresh session for 2FA enabled user. Login required");
} else {
Timber.e("Error occurred in refreshing session. Error code is %s", loginResultCode);
}
} else {
Timber.e("Error occurred while fetching auth token. Error code is %s and message is %s",
result.getString("api/error/@code"),
result.getString("api/error/@info"));
}
return centralAuthToken;
}
@Override
public boolean fileExistsWithName(String fileName) throws IOException {
return api.action("query")
.param("prop", "imageinfo")
.param("titles", "File:" + fileName)
.get()
.getNodes("/api/query/pages/page/imageinfo").size() > 0;
}
@Override
public Single<Boolean> pageExists(String pageName) {
return Single.fromCallable(() -> Double.parseDouble(api.action("query")
.param("titles", pageName)
.get()
.getString("/api/query/pages/page/@_idx")) != -1);
}
@Override
public boolean thank(String editToken, long revision) throws IOException {
CustomApiResult res = api.action("thank")
.param("rev", revision)
.param("token", editToken)
.param("source", CommonsApplication.getInstance().getUserAgent())
.post();
String r = res.getString("/api/result/@success");
// Does this correctly check the success/failure?
// The docs https://www.mediawiki.org/wiki/Extension:Thanks seems unclear about that.
return r.equals("success");
}
@Override
@Nullable
public String edit(String editToken, String processedPageContent, String filename, String summary) throws IOException {
return api.action("edit")
.param("title", filename)
.param("token", getEditToken())
.param("text", processedPageContent)
.param("summary", summary)
.post()
.getString("/api/edit/@result");
}
@Override
@Nullable
public String appendEdit(String editToken, String processedPageContent, String filename, String summary) throws IOException {
return api.action("edit")
.param("title", filename)
.param("token", getEditToken())
.param("appendtext", processedPageContent)
.param("summary", summary)
.post()
.getString("/api/edit/@result");
}
@Override
@Nullable
public String prependEdit(String editToken, String processedPageContent, String filename, String summary) throws IOException {
return api.action("edit")
.param("title", filename)
.param("token", getEditToken())
.param("prependtext", processedPageContent)
.param("summary", summary)
.post()
.getString("/api/edit/@result");
}
@Override
public Single<String> parseWikicode(String source) {
return Single.fromCallable(() -> api.action("flow-parsoid-utils")
.param("from", "wikitext")
.param("to", "html")
.param("content", source)
.param("title", "Main_page")
.get()
.getString("/api/flow-parsoid-utils/@content"));
}
@Override
@NonNull
public Single<MediaResult> fetchMediaByFilename(String filename) {
return Single.fromCallable(() -> {
CustomApiResult apiResult = api.action("query")
.param("prop", "revisions")
.param("titles", filename)
.param("rvprop", "content")
.param("rvlimit", 1)
.param("rvgeneratexml", 1)
.get();
return new MediaResult(
apiResult.getString("/api/query/pages/page/revisions/rev"),
apiResult.getString("/api/query/pages/page/revisions/rev/@parsetree"));
});
}
@Override
@NonNull
public Observable<String> searchCategories(String filterValue, int searchCatsLimit) {
List<String> categories = new ArrayList<>();
return Single.fromCallable(() -> {
List<CustomApiResult> categoryNodes = null;
try {
categoryNodes = api.action("query")
.param("format", "xml")
.param("list", "search")
.param("srwhat", "text")
.param("srnamespace", "14")
.param("srlimit", searchCatsLimit)
.param("srsearch", filterValue)
.get()
.getNodes("/api/query/search/p/@title");
} catch (IOException e) {
Timber.e(e, "Failed to obtain searchCategories");
}
if (categoryNodes == null) {
return new ArrayList<String>();
}
for (CustomApiResult categoryNode : categoryNodes) {
String cat = categoryNode.getDocument().getTextContent();
String catString = cat.replace("Category:", "");
if (!categories.contains(catString)) {
categories.add(catString);
}
}
return categories;
}).flatMapObservable(Observable::fromIterable);
}
@Override
@NonNull
public Observable<String> allCategories(String filterValue, int searchCatsLimit) {
return Single.fromCallable(() -> {
ArrayList<CustomApiResult> categoryNodes = null;
try {
categoryNodes = api.action("query")
.param("list", "allcategories")
.param("acprefix", filterValue)
.param("aclimit", searchCatsLimit)
.get()
.getNodes("/api/query/allcategories/c");
} catch (IOException e) {
Timber.e(e, "Failed to obtain allCategories");
}
if (categoryNodes == null) {
return new ArrayList<String>();
}
List<String> categories = new ArrayList<>();
for (CustomApiResult categoryNode : categoryNodes) {
categories.add(categoryNode.getDocument().getTextContent());
}
return categories;
}).flatMapObservable(Observable::fromIterable);
}
@Override
public String getWikidataCsrfToken() throws IOException {
String wikidataCsrfToken = wikidataApi.action("query")
.param("action", "query")
.param("centralauthtoken", getCentralAuthToken())
.param("meta", "tokens")
.post()
.getString("/api/query/tokens/@csrftoken");
Timber.d("Wikidata csrf token is %s", wikidataCsrfToken);
return wikidataCsrfToken;
}
/**
* Creates a new claim using the wikidata API
* https://www.mediawiki.org/wiki/Wikibase/API
* @param entityId the wikidata entity to be edited
* @param property the property to be edited, for eg P18 for images
* @param snaktype the type of value stored for that property
* @param value the actual value to be stored for the property, for eg filename in case of P18
* @return returns revisionId if the claim is successfully created else returns null
* @throws IOException
*/
@Nullable
@Override
public String wikidataCreateClaim(String entityId, String property, String snaktype, String value) throws IOException {
Timber.d("Filename is %s", value);
CustomApiResult result = wikidataApi.action("wbcreateclaim")
.param("entity", entityId)
.param("centralauthtoken", getCentralAuthToken())
.param("token", getWikidataCsrfToken())
.param("snaktype", snaktype)
.param("property", property)
.param("value", value)
.post();
if (result == null || result.getNode("api") == null) {
return null;
}
Node node = result.getNode("api").getDocument();
Element element = (Element) node;
if (element != null && element.getAttribute("success").equals("1")) {
return result.getString("api/pageinfo/@lastrevid");
} else {
Timber.e(result.getString("api/error/@code") + " " + result.getString("api/error/@info"));
}
return null;
}
/**
* Adds the wikimedia-commons-app tag to the edits made on wikidata
* @param revisionId
* @return
* @throws IOException
*/
@Nullable
@Override
public boolean addWikidataEditTag(String revisionId) throws IOException {
CustomApiResult result = wikidataApi.action("tag")
.param("revid", revisionId)
.param("centralauthtoken", getCentralAuthToken())
.param("token", getWikidataCsrfToken())
.param("add", "wikimedia-commons-app")
.param("reason", "Add tag for edits made using Android Commons app")
.post();
if (result == null || result.getNode("api") == null) {
return false;
}
if ("success".equals(result.getString("api/tag/result/@status"))) {
return true;
} else {
Timber.e("Error occurred in creating claim. Error code is: %s and message is %s",
result.getString("api/error/@code"),
result.getString("api/error/@info"));
}
return false;
}
@Override
@NonNull
public Observable<String> searchTitles(String title, int searchCatsLimit) {
return Single.fromCallable((Callable<List<String>>) () -> {
ArrayList<CustomApiResult> categoryNodes;
try {
categoryNodes = api.action("query")
.param("format", "xml")
.param("list", "search")
.param("srwhat", "text")
.param("srnamespace", "14")
.param("srlimit", searchCatsLimit)
.param("srsearch", title)
.get()
.getNodes("/api/query/search/p/@title");
} catch (IOException e) {
Timber.e(e, "Failed to obtain searchTitles");
return Collections.emptyList();
}
if (categoryNodes == null) {
return Collections.emptyList();
}
List<String> titleCategories = new ArrayList<>();
for (CustomApiResult categoryNode : categoryNodes) {
String cat = categoryNode.getDocument().getTextContent();
String catString = cat.replace("Category:", "");
titleCategories.add(catString);
}
return titleCategories;
}).flatMapObservable(Observable::fromIterable);
}
@Override
@NonNull
public LogEventResult logEvents(String user, String lastModified, String queryContinue, int limit) throws IOException {
CustomMwApi.RequestBuilder builder = api.action("query")
.param("list", "logevents")
.param("letype", "upload")
.param("leprop", "title|timestamp|ids")
.param("leuser", user)
.param("lelimit", limit);
if (!TextUtils.isEmpty(lastModified)) {
builder.param("leend", lastModified);
}
if (!TextUtils.isEmpty(queryContinue)) {
builder.param("lestart", queryContinue);
}
CustomApiResult result = builder.get();
return new LogEventResult(
getLogEventsFromResult(result),
result.getString("/api/query-continue/logevents/@lestart"));
}
@NonNull
private ArrayList<LogEventResult.LogEvent> getLogEventsFromResult(CustomApiResult result) {
ArrayList<CustomApiResult> uploads = result.getNodes("/api/query/logevents/item");
Timber.d("%d results!", uploads.size());
ArrayList<LogEventResult.LogEvent> logEvents = new ArrayList<>();
for (CustomApiResult image : uploads) {
logEvents.add(new LogEventResult.LogEvent(
image.getString("@pageid"),
image.getString("@title"),
parseMWDate(image.getString("@timestamp")))
);
}
return logEvents;
}
@Override
@Nullable
public String revisionsByFilename(String filename) throws IOException {
return api.action("query")
.param("prop", "revisions")
.param("rvprop", "timestamp|content")
.param("titles", filename)
.get()
.getString("/api/query/pages/page/revisions/rev");
}
@Override
@NonNull
public List<Notification> getNotifications(boolean archived) {
CustomApiResult notificationNode = null;
String notfilter;
try {
if (archived) {
notfilter = "read";
}else {
notfilter = "!read";
}
String language=Locale.getDefault().getLanguage();
if(StringUtils.isBlank(language)){
//if no language is set we use the default user language defined on wikipedia
language="user";
}
notificationNode = api.action("query")
.param("notprop", "list")
.param("format", "xml")
.param("meta", "notifications")
.param("notformat", "model")
.param("notwikis", "wikidatawiki|commonswiki|enwiki")
.param("notfilter", notfilter)
.param("uselang", language)
.get()
.getNode("/api/query/notifications/list");
} catch (IOException e) {
Timber.e(e, "Failed to obtain searchCategories");
}
if (notificationNode == null
|| notificationNode.getDocument() == null
|| notificationNode.getDocument().getChildNodes() == null
|| notificationNode.getDocument().getChildNodes().getLength() == 0) {
return new ArrayList<>();
}
NodeList childNodes = notificationNode.getDocument().getChildNodes();
return NotificationUtils.getNotificationsFromList(context, childNodes);
}
@Override
public boolean markNotificationAsRead(Notification notification) throws IOException {
Timber.d("Trying to mark notification as read: %s", notification.toString());
String result = api.action("echomarkread")
.param("token", getEditToken())
.param("list", notification.notificationId)
.post()
.getString("/api/query/echomarkread/@result");
if (StringUtils.isBlank(result)) {
return false;
}
return result.equals("success");
}
/**
* The method takes categoryName as input and returns a List of Subcategories
* It uses the generator query API to get the subcategories in a category, 500 at a time.
* Uses the query continue values for fetching paginated responses
* @param categoryName Category name as defined on commons
* @return
*/
@Override
@NonNull
public List<String> getSubCategoryList(String categoryName) {
CustomApiResult apiResult = null;
try {
CustomMwApi.RequestBuilder requestBuilder = api.action("query")
.param("generator", "categorymembers")
.param("format", "xml")
.param("gcmtype","subcat")
.param("gcmtitle", categoryName)
.param("prop", "info")
.param("gcmlimit", "500")
.param("iiprop", "url|extmetadata");
apiResult = requestBuilder.get();
} catch (IOException e) {
Timber.e(e, "Failed to obtain searchCategories");
}
if (apiResult == null) {
return new ArrayList<>();
}
CustomApiResult categoryImagesNode = apiResult.getNode("/api/query/pages");
if (categoryImagesNode == null
|| categoryImagesNode.getDocument() == null
|| categoryImagesNode.getDocument().getChildNodes() == null
|| categoryImagesNode.getDocument().getChildNodes().getLength() == 0) {
return new ArrayList<>();
}
NodeList childNodes = categoryImagesNode.getDocument().getChildNodes();
return CategoryImageUtils.getSubCategoryList(childNodes);
}
/**
* The method takes categoryName as input and returns a List of parent categories
* It uses the generator query API to get the parent categories of a category, 500 at a time.
* @param categoryName Category name as defined on commons
* @return
*/
@Override
@NonNull
public List<String> getParentCategoryList(String categoryName) {
CustomApiResult apiResult = null;
try {
CustomMwApi.RequestBuilder requestBuilder = api.action("query")
.param("generator", "categories")
.param("format", "xml")
.param("titles", categoryName)
.param("prop", "info")
.param("cllimit", "500")
.param("iiprop", "url|extmetadata");
apiResult = requestBuilder.get();
} catch (IOException e) {
Timber.e(e, "Failed to obtain parent Categories");
}
if (apiResult == null) {
return new ArrayList<>();
}
CustomApiResult categoryImagesNode = apiResult.getNode("/api/query/pages");
if (categoryImagesNode == null
|| categoryImagesNode.getDocument() == null
|| categoryImagesNode.getDocument().getChildNodes() == null
|| categoryImagesNode.getDocument().getChildNodes().getLength() == 0) {
return new ArrayList<>();
}
NodeList childNodes = categoryImagesNode.getDocument().getChildNodes();
return CategoryImageUtils.getSubCategoryList(childNodes);
}
/**
* This method takes search keyword as input and returns a list of categories objects filtered using search query
* It uses the generator query API to get the categories searched using a query, 25 at a time.
* @param query keyword to search categories on commons
* @return
*/
@Override
@NonNull
public List<String> searchCategory(String query, int offset) {
List<CustomApiResult> categoryNodes = null;
try {
categoryNodes = api.action("query")
.param("format", "xml")
.param("list", "search")
.param("srwhat", "text")
.param("srnamespace", "14")
.param("srlimit", "25")
.param("sroffset",offset)
.param("srsearch", query)
.get()
.getNodes("/api/query/search/p/@title");
} catch (IOException e) {
Timber.e(e, "Failed to obtain searchCategories");
}
if (categoryNodes == null) {
return new ArrayList<>();
}
List<String> categories = new ArrayList<>();
for (CustomApiResult categoryNode : categoryNodes) {
String catName = categoryNode.getDocument().getTextContent();
categories.add(catName);
}
return categories;
}
/**
* For APIs that return paginated responses, MediaWiki APIs uses the QueryContinue to facilitate fetching of subsequent pages
* https://www.mediawiki.org/wiki/API:Raw_query_continue
* After fetching images a page of image for a particular category, shared defaultKvStore are updated with the latest QueryContinue Values
* @param keyword
* @param queryContinue
*/
private void setQueryContinueValues(String keyword, QueryContinue queryContinue) {
defaultKvStore.putString(keyword, gson.toJson(queryContinue));
}
/**
* Before making a paginated API call, this method is called to get the latest query continue values to be used
* @param keyword
* @return
*/
@Nullable
private QueryContinue getQueryContinueValues(String keyword) {
String queryContinueString = defaultKvStore.getString(keyword, null);
return gson.fromJson(queryContinueString, QueryContinue.class);
}
@Override
public boolean existingFile(String fileSha1) throws IOException {
return api.action("query")
.param("format", "xml")
.param("list", "allimages")
.param("aisha1", fileSha1)
.get()
.getNodes("/api/query/allimages/img").size() > 0;
}
@Override
@NonNull
public Single<UploadStash> uploadFile(
String filename,
@NonNull InputStream file,
long dataLength,
Uri fileUri,
Uri contentProviderUri,
ProgressListener progressListener) {
return Single.fromCallable(() -> {
CustomApiResult result = api.uploadToStash(filename, file, dataLength, getEditToken(), progressListener::onProgress);
Timber.wtf("Result: " + result.toString());
String resultStatus = result.getString("/api/upload/@result");
if (!resultStatus.equals("Success")) {
String errorCode = result.getString("/api/error/@code");
Timber.e(errorCode);
if (errorCode.equals(ERROR_CODE_BAD_TOKEN)) {
ViewUtil.showLongToast(context, R.string.bad_token_error_proposed_solution);
}
return new UploadStash(errorCode, resultStatus, filename, "");
} else {
String filekey = result.getString("/api/upload/@filekey");
return new UploadStash("", resultStatus, filename, filekey);
}
});
}
@Override
@NonNull
public Single<UploadResult> uploadFileFinalize(
String filename,
String filekey,
String pageContents,
String editSummary) throws IOException {
return Single.fromCallable(() -> {
CustomApiResult result = api.uploadFromStash(
filename, filekey, pageContents, editSummary,
getEditToken());
Timber.d("Result: %s", result.toString());
String resultStatus = result.getString("/api/upload/@result");
if (!resultStatus.equals("Success")) {
String errorCode = result.getString("/api/error/@code");
Timber.e(errorCode);
if (errorCode.equals(ERROR_CODE_BAD_TOKEN)) {
ViewUtil.showLongToast(context, R.string.bad_token_error_proposed_solution);
}
return new UploadResult(resultStatus, errorCode);
} else {
Date dateUploaded = parseMWDate(result.getString("/api/upload/imageinfo/@timestamp"));
String canonicalFilename = "File:" + result.getString("/api/upload/@filename")
.replace("_", " ")
.trim(); // Title vs Filename
String imageUrl = result.getString("/api/upload/imageinfo/@url");
return new UploadResult(resultStatus, dateUploaded, canonicalFilename, imageUrl);
}
});
}
/**
* Checks to see if a user is currently blocked from Commons
* @return whether or not the user is blocked from Commons
*/
@Override
public boolean isUserBlockedFromCommons() {
boolean userBlocked = false;
try {
CustomApiResult result = api.action("query")
.param("action", "query")
.param("format", "xml")
.param("meta", "userinfo")
.param("uiprop", "blockinfo")
.get();
if (result != null) {
String blockEnd = result.getString("/api/query/userinfo/@blockexpiry");
if (blockEnd.equals("infinite")) {
userBlocked = true;
} else if (!blockEnd.isEmpty()) {
Date endDate = parseMWDate(blockEnd);
Date current = new Date();
userBlocked = endDate.after(current);
}
}
} catch (Exception e) {
e.printStackTrace();
}
return userBlocked;
}
private Date parseMWDate(String mwDate) {
try {
return DateUtil.iso8601DateParse(mwDate);
} catch (ParseException e) {
throw new RuntimeException(e);
}
}
/**
* Calls media wiki's logout API
*/
public void logout() {
try {
api.logout();
} catch (IOException e) {
Timber.e(e, "Error occurred while logging out");
}
}
}

View file

@ -1,122 +0,0 @@
package fr.free.nrw.commons.mwapi;
import org.apache.http.client.HttpClient;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
import java.io.IOError;
import java.io.IOException;
import java.io.StringWriter;
import java.util.ArrayList;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import in.yuvi.http.fluent.Http;
import timber.log.Timber;
public class CustomApiResult {
private Node doc;
private XPath evaluator;
CustomApiResult(Node doc) {
this.doc = doc;
this.evaluator = XPathFactory.newInstance().newXPath();
}
static CustomApiResult fromRequestBuilder(String requestIdentifier, Http.HttpRequestBuilder builder, HttpClient client) throws IOException {
try {
DocumentBuilder docBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
Document doc = docBuilder.parse(builder.use(client).charset("utf-8").data("format", "xml").asResponse().getEntity().getContent());
printStringFromDocument(requestIdentifier, doc);
return new CustomApiResult(doc);
} catch (ParserConfigurationException e) {
// I don't know wtf I can do about this on...
Timber.e(e, "Error occurred while parsing the response for method %s", requestIdentifier);
throw new RuntimeException(e);
} catch (IllegalStateException e) {
// So, this should never actually happen - since we assume MediaWiki always generates valid json
// So the only thing causing this would be a network truncation
// Sooo... I can throw IOError
// Thanks Java, for making me spend significant time on shit that happens once in a bluemoon
// I surely am writing Nuclear Submarine controller code
Timber.e(e, "Error occurred while parsing the response for method %s", requestIdentifier);
throw new IOError(e);
} catch (SAXException e) {
// See Rant above
Timber.e(e, "Error occurred while parsing the response for method %s", requestIdentifier);
throw new IOError(e);
}
}
public static void printStringFromDocument(String requestIdentifier, Document doc)
{
try
{
DOMSource domSource = new DOMSource(doc);
StringWriter writer = new StringWriter();
StreamResult result = new StreamResult(writer);
TransformerFactory tf = TransformerFactory.newInstance();
Transformer transformer = tf.newTransformer();
transformer.transform(domSource, result);
Timber.d("API response for method %s is\n %s", requestIdentifier, writer.toString());
}
catch(TransformerException ex)
{
Timber.e(ex, "Error occurred in transforming response for method %s", requestIdentifier);
}
}
public Node getDocument() {
return doc;
}
public ArrayList<CustomApiResult> getNodes(String xpath) {
try {
ArrayList<CustomApiResult> results = new ArrayList<>();
NodeList nodes = (NodeList) evaluator.evaluate(xpath, doc, XPathConstants.NODESET);
for(int i = 0; i < nodes.getLength(); i++) {
results.add(new CustomApiResult(nodes.item(i)));
}
return results;
} catch (XPathExpressionException e) {
return null;
}
}
public CustomApiResult getNode(String xpath) {
try {
return new CustomApiResult((Node) evaluator.evaluate(xpath, doc, XPathConstants.NODE));
} catch (XPathExpressionException e) {
return null;
}
}
public Double getNumber(String xpath) {
try {
return (Double) evaluator.evaluate(xpath, doc, XPathConstants.NUMBER);
} catch (XPathExpressionException e) {
return null;
}
}
public String getString(String xpath) {
try {
return (String) evaluator.evaluate(xpath, doc, XPathConstants.STRING);
} catch (XPathExpressionException e) {
return null;
}
}
}

View file

@ -1,188 +0,0 @@
package fr.free.nrw.commons.mwapi;
import org.apache.http.cookie.Cookie;
import org.apache.http.impl.client.AbstractHttpClient;
import org.apache.http.impl.cookie.BasicClientCookie;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashMap;
import java.util.List;
import in.yuvi.http.fluent.Http;
import in.yuvi.http.fluent.ProgressListener;
import timber.log.Timber;
public class CustomMwApi {
public class RequestBuilder {
private HashMap<String, Object> params;
private CustomMwApi api;
RequestBuilder(CustomMwApi api) {
params = new HashMap<>();
this.api = api;
}
public RequestBuilder param(String key, Object value) {
params.put(key, value);
return this;
}
public CustomApiResult get() throws IOException {
return api.makeRequest("GET", params);
}
public CustomApiResult post() throws IOException {
return api.makeRequest("POST", params);
}
}
private AbstractHttpClient client;
private String apiURL;
public boolean isLoggedIn;
private String authCookie = null;
private String userName = null;
private String userID = null;
public CustomMwApi(String apiURL, AbstractHttpClient client) {
this.apiURL = apiURL;
this.client = client;
}
public RequestBuilder action(String action) {
RequestBuilder builder = new RequestBuilder(this);
builder.param("action", action);
return builder;
}
public String getAuthCookie() {
if (authCookie == null){
authCookie = "";
List<Cookie> cookies = client.getCookieStore().getCookies();
for(Cookie cookie: cookies) {
authCookie += cookie.getName() + "=" + cookie.getValue() + ";";
}
}
return authCookie;
}
public void setAuthCookie(String authCookie) {
if (authCookie == null) {//If the authCookie is null, no need to proceed
return;
}
this.authCookie = authCookie;
this.isLoggedIn = true;
String[] cookies = authCookie.split(";");
String domain;
try {
domain = new URL(apiURL).getHost();
} catch (MalformedURLException e) {
// Mighty well better not happen!
e.printStackTrace();
throw new RuntimeException(e);
}
// This works because I know which cookies are going to be set by MediaWiki, and they don't contain a = or ; in them :D
for(String cookie: cookies) {
String[] parts = cookie.split("=");
BasicClientCookie c = new BasicClientCookie(parts[0], parts[1]);
c.setDomain(domain);
client.getCookieStore().addCookie(c);
}
}
public void removeAllCookies() {
client.getCookieStore().clear();
}
public boolean validateLogin() throws IOException {
CustomApiResult userMeta = this.action("query").param("meta", "userinfo").get();
this.userID = userMeta.getString("/api/query/userinfo/@id");
this.userName = userMeta.getString("/api/query/userinfo/@name");
Timber.d("User id is %s and user name is %s", userID, userName);
return !userID.equals("0");
}
public String getUserID() throws IOException {
if (this.userID == null || this.userID.equals("0")) {
this.validateLogin();
}
return userID;
}
public String getUserName() throws IOException {
if (this.userID == null || this.userID.equals("0")) {
this.validateLogin();
}
return userName;
}
public String login(String username, String password) throws IOException {
CustomApiResult tokenData = this.action("login").param("lgname", username).param("lgpassword", password).post();
String result = tokenData.getString("/api/login/@result");
if (result.equals("NeedToken")) {
String token = tokenData.getString("/api/login/@token");
CustomApiResult confirmData = this.action("login").param("lgname", username).param("lgpassword", password).param("lgtoken", token).post();
String finalResult = confirmData.getString("/api/login/@result");
if (finalResult.equals("Success")) {
isLoggedIn = true;
}
return finalResult;
} else {
return result;
}
}
public CustomApiResult uploadToStash(String filename, InputStream file, long length, String token, ProgressListener uploadProgressListener) throws IOException {
Timber.d("Initiating upload for file %s", filename);
Http.HttpRequestBuilder builder = Http.multipart(apiURL)
.data("action", "upload")
.data("stash", "1")
.data("token", token)
.data("ignorewarnings", "1")
.data("filename", filename)
.sendProgressListener(uploadProgressListener);
if (length != -1) {
builder.file("file", filename, file, length);
} else {
builder.file("file", filename, file);
}
return CustomApiResult.fromRequestBuilder("uploadToStash", builder, client);
}
public CustomApiResult uploadFromStash(String filename, String filekey, String text, String comment, String token) throws IOException {
Http.HttpRequestBuilder builder = Http.multipart(apiURL)
.data("action", "upload")
.data("token", token)
.data("ignorewarnings", "1")
.data("text", text)
.data("comment", comment)
.data("filename", filename)
.data("filekey", filekey);
return CustomApiResult.fromRequestBuilder("uploadFromStash", builder, client);
}
public void logout() throws IOException {
// I should be doing more validation here, but meh
isLoggedIn = false;
this.action("logout").post();
removeAllCookies();
authCookie = null;
}
private CustomApiResult makeRequest(String method, HashMap<String, Object> params) throws IOException {
Http.HttpRequestBuilder builder;
if (method.equals("POST")) {
builder = Http.post(apiURL);
} else {
builder = Http.get(apiURL);
}
builder.data(params);
return CustomApiResult.fromRequestBuilder(apiURL, builder, client);
}
}
;

View file

@ -1,51 +0,0 @@
package fr.free.nrw.commons.mwapi;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.Date;
import java.util.List;
public class LogEventResult {
private final List<LogEvent> logEvents;
private final String queryContinue;
LogEventResult(@NonNull List<LogEvent> logEvents, String queryContinue) {
this.logEvents = logEvents;
this.queryContinue = queryContinue;
}
@NonNull
public List<LogEvent> getLogEvents() {
return logEvents;
}
@Nullable
public String getQueryContinue() {
return queryContinue;
}
public static class LogEvent {
private final String pageId;
private final String filename;
private final Date dateUpdated;
LogEvent(String pageId, String filename, Date dateUpdated) {
this.pageId = pageId;
this.filename = filename;
this.dateUpdated = dateUpdated;
}
public boolean isDeleted() {
return pageId.equals("0");
}
public String getFilename() {
return filename;
}
public Date getDateUpdated() {
return dateUpdated;
}
}
}

View file

@ -1,33 +0,0 @@
package fr.free.nrw.commons.mwapi;
public class MediaResult {
private final String wikiSource;
private final String parseTreeXmlSource;
/**
* Full-fledged constructor of MediaResult
*
* @param wikiSource Media wiki source
* @param parseTreeXmlSource Media tree parsed in XML
*/
MediaResult(String wikiSource, String parseTreeXmlSource) {
this.wikiSource = wikiSource;
this.parseTreeXmlSource = parseTreeXmlSource;
}
/**
* Gets wiki source
* @return Wiki source
*/
public String getWikiSource() {
return wikiSource;
}
/**
* Gets tree parsed in XML
* @return XML parsed tree
*/
public String getParseTreeXmlSource() {
return parseTreeXmlSource;
}
}

View file

@ -1,106 +0,0 @@
package fr.free.nrw.commons.mwapi;
import android.net.Uri;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import fr.free.nrw.commons.notification.Notification;
import io.reactivex.Observable;
import io.reactivex.Single;
public interface MediaWikiApi {
String getAuthCookie();
void setAuthCookie(String authCookie);
String login(String username, String password) throws IOException;
String login(String username, String password, String twoFactorCode) throws IOException;
boolean validateLogin() throws IOException;
String getEditToken() throws IOException;
String getWikidataCsrfToken() throws IOException;
String getCentralAuthToken() throws IOException;
boolean fileExistsWithName(String fileName) throws IOException;
Single<Boolean> pageExists(String pageName);
List<String> getSubCategoryList(String categoryName);
List<String> getParentCategoryList(String categoryName);
@NonNull
List<String> searchCategory(String title, int offset);
@NonNull
Single<UploadStash> uploadFile(String filename, InputStream file,
long dataLength, Uri fileUri, Uri contentProviderUri,
final ProgressListener progressListener);
@NonNull
Single<UploadResult> uploadFileFinalize(String filename, String filekey,
String pageContents, String editSummary) throws IOException;
@Nullable
String edit(String editToken, String processedPageContent, String filename, String summary) throws IOException;
@Nullable
String prependEdit(String editToken, String processedPageContent, String filename, String summary) throws IOException;
@Nullable
String appendEdit(String editToken, String processedPageContent, String filename, String summary) throws IOException;
@Nullable
String wikidataCreateClaim(String entityId, String property, String snaktype, String value) throws IOException;
@Nullable
boolean addWikidataEditTag(String revisionId) throws IOException;
Single<String> parseWikicode(String source);
@NonNull
Single<MediaResult> fetchMediaByFilename(String filename);
@NonNull
Observable<String> searchCategories(String filterValue, int searchCatsLimit);
@NonNull
Observable<String> allCategories(String filter, int searchCatsLimit);
@NonNull
List<Notification> getNotifications(boolean archived) throws IOException;
@NonNull
boolean markNotificationAsRead(Notification notification) throws IOException;
@NonNull
Observable<String> searchTitles(String title, int searchCatsLimit);
@Nullable
String revisionsByFilename(String filename) throws IOException;
boolean existingFile(String fileSha1) throws IOException;
@NonNull
LogEventResult logEvents(String user, String lastModified, String queryContinue, int limit) throws IOException;
boolean isUserBlockedFromCommons();
void logout();
// Single<CampaignResponseDTO> getCampaigns();
boolean thank(String editToken, long revision) throws IOException;
interface ProgressListener {
void onProgress(long transferred, long total);
}
}

View file

@ -1,85 +0,0 @@
package fr.free.nrw.commons.mwapi;
import androidx.annotation.NonNull;
import org.apache.http.Header;
import org.apache.http.HttpRequest;
import org.apache.http.HttpRequestInterceptor;
import org.apache.http.impl.client.ClientParamsStack;
import org.apache.http.params.HttpParamsNames;
import org.apache.http.protocol.HttpContext;
import java.util.HashSet;
import java.util.Set;
import timber.log.Timber;
public class NetworkInterceptors {
/**
* Interceptor to log the HTTP request
*/
@NonNull
public static HttpRequestInterceptor getHttpRequestInterceptor() {
return (HttpRequest request, HttpContext httpContext) -> {
Timber.v("<<<<<<<<<<<<<< START OF REQUEST LOGGING [%s] >>>>>>>>>>>>", request.getRequestLine().getUri());
Timber.v("Request line:\n %s", request.getRequestLine().toString());
logRequestParams(request);
logRequestHeaders(request);
Timber.v("Protocol version:\n %s", request.getProtocolVersion());
Timber.v("<<<<<<<<<<<<<< END OF REQUEST LOGGING [%s] >>>>>>>>>>>>", request.getRequestLine().getUri());
};
}
/**
* Log all request params from a HTTPRequest
* @param request
*/
private static void logRequestParams(HttpRequest request) {
Set<String> names = new HashSet<>();
if (request.getParams() instanceof ClientParamsStack) {
ClientParamsStack cps = (ClientParamsStack) request.getParams();
if (cps.getApplicationParams() != null
&& cps.getRequestParams() instanceof HttpParamsNames) {
names.addAll(((HttpParamsNames) cps.getApplicationParams()).getNames());
}
if (cps.getClientParams() != null
&& cps.getClientParams() instanceof HttpParamsNames) {
names.addAll(((HttpParamsNames) cps.getClientParams()).getNames());
}
if (cps.getRequestParams() != null
&& cps.getRequestParams() instanceof HttpParamsNames) {
names.addAll(((HttpParamsNames) cps.getRequestParams()).getNames());
}
if (cps.getOverrideParams() != null
&& cps.getRequestParams() instanceof HttpParamsNames) {
names.addAll(((HttpParamsNames) cps.getOverrideParams()).getNames());
}
} else {
HttpParamsNames params = (HttpParamsNames) request.getParams();
names = params.getNames();
}
Timber.v("<<<<<<<<<<<<<< REQUEST PARAMS >>>>>>>>>>>>");
for (String name : names) {
Timber.v("Param >> %s: %s", name, request.getParams().getParameter(name));
}
Timber.v("<<<<<<<<<<<<<< REQUEST PARAMS >>>>>>>>>>>>");
}
/**
* Log all headers from a HTTPRequest
* @param request
*/
private static void logRequestHeaders(HttpRequest request) {
Header[] headerFields = request.getAllHeaders();
Timber.v("<<<<<<<<<<<<<< HEADERS >>>>>>>>>>>>");
for (int e = 0; e < request.getAllHeaders().length; e++) {
Timber.v("Header >> %s: %s", headerFields[e].getName(), headerFields[e].getValue());
}
Timber.v("<<<<<<<<<<<<<< HEADERS >>>>>>>>>>>>");
}
}

View file

@ -46,15 +46,11 @@ import timber.log.Timber;
public class OkHttpJsonApiClient {
private static final String THUMB_SIZE = "640";
public static final Type mapType = new TypeToken<Map<String, String>>() {
}.getType();
private final OkHttpClient okHttpClient;
private final HttpUrl wikiMediaToolforgeUrl;
private final String sparqlQueryUrl;
private final String campaignsUrl;
private final String commonsBaseUrl;
private final JsonKvStore defaultKvStore;
private Gson gson;
@ -64,14 +60,12 @@ public class OkHttpJsonApiClient {
String sparqlQueryUrl,
String campaignsUrl,
String commonsBaseUrl,
JsonKvStore defaultKvStore,
Gson gson) {
this.okHttpClient = okHttpClient;
this.wikiMediaToolforgeUrl = wikiMediaToolforgeUrl;
this.sparqlQueryUrl = sparqlQueryUrl;
this.campaignsUrl = campaignsUrl;
this.commonsBaseUrl = commonsBaseUrl;
this.defaultKvStore = defaultKvStore;
this.gson = gson;
}
@ -234,56 +228,6 @@ public class OkHttpJsonApiClient {
});
}
/**
* The method returns the picture of the day
*
* @return Media object corresponding to the picture of the day
*/
@Nullable
public Single<Media> getPictureOfTheDay() {
String date = CommonsDateUtil.getIso8601DateFormatShort().format(new Date());
Timber.d("Current date is %s", date);
String template = "Template:Potd/" + date;
return getMedia(template, true);
}
/**
* Fetches Media object from the imageInfo API
*
* @param titles the tiles to be searched for. Can be filename or template name
* @param useGenerator specifies if a image generator parameter needs to be passed or not
* @return
*/
public Single<Media> getMedia(String titles, boolean useGenerator) {
HttpUrl.Builder urlBuilder = HttpUrl
.parse(commonsBaseUrl)
.newBuilder()
.addQueryParameter("action", "query")
.addQueryParameter("format", "json")
.addQueryParameter("formatversion", "2")
.addQueryParameter("titles", titles);
if (useGenerator) {
urlBuilder.addQueryParameter("generator", "images");
}
Request request = new Request.Builder()
.url(appendMediaProperties(urlBuilder).build())
.build();
return Single.fromCallable(() -> {
Response response = okHttpClient.newCall(request).execute();
if (response.body() != null && response.isSuccessful()) {
String json = response.body().string();
MwQueryResponse mwQueryPage = gson.fromJson(json, MwQueryResponse.class);
if (mwQueryPage.success() && mwQueryPage.query().firstPage() != null) {
return Media.from(mwQueryPage.query().firstPage());
}
}
return null;
});
}
/**
* Whenever imageInfo is fetched, these common properties can be specified for the API call
* https://www.mediawiki.org/wiki/API:Imageinfo
@ -304,124 +248,4 @@ public class OkHttpJsonApiClient {
return builder;
}
/**
* This method takes the keyword and queryType as input and returns a list of Media objects filtered using image generator query
* It uses the generator query API to get the images searched using a query, 10 at a time.
* @param queryType queryType can be "search" OR "category"
* @param keyword the search keyword. Can be either category name or search query
* @return
*/
@Nullable
public Single<List<Media>> getMediaList(String queryType, String keyword) {
HttpUrl.Builder urlBuilder = HttpUrl
.parse(commonsBaseUrl)
.newBuilder()
.addQueryParameter("action", "query")
.addQueryParameter("format", "json")
.addQueryParameter("formatversion", "2");
if (queryType.equals("search")) {
appendSearchParam(keyword, urlBuilder);
} else {
appendCategoryParams(keyword, urlBuilder);
}
appendQueryContinueValues(keyword, urlBuilder);
Request request = new Request.Builder()
.url(appendMediaProperties(urlBuilder).build())
.build();
return Single.fromCallable(() -> {
Response response = okHttpClient.newCall(request).execute();
List<Media> mediaList = new ArrayList<>();
if (response.body() != null && response.isSuccessful()) {
String json = response.body().string();
MwQueryResponse mwQueryResponse = gson.fromJson(json, MwQueryResponse.class);
if (null == mwQueryResponse
|| null == mwQueryResponse.query()
|| null == mwQueryResponse.query().pages()) {
return mediaList;
}
putContinueValues(keyword, mwQueryResponse.continuation());
List<MwQueryPage> pages = mwQueryResponse.query().pages();
for (MwQueryPage page : pages) {
Media media = Media.from(page);
if (media != null) {
mediaList.add(media);
}
}
}
return mediaList;
});
}
/**
* Append params for search query.
*
* @param query the search query to be sent to the API
* @param urlBuilder builder for HttpUrl
*/
private void appendSearchParam(String query, HttpUrl.Builder urlBuilder) {
urlBuilder.addQueryParameter("generator", "search")
.addQueryParameter("gsrwhat", "text")
.addQueryParameter("gsrnamespace", "6")
.addQueryParameter("gsrlimit", "25")
.addQueryParameter("gsrsearch", query);
}
/**
* It takes a urlBuilder and appends all the continue values as query parameters
*
* @param query
* @param urlBuilder
*/
private void appendQueryContinueValues(String query, HttpUrl.Builder urlBuilder) {
Map<String, String> continueValues = getContinueValues(query);
if (continueValues != null && continueValues.size() > 0) {
for (Map.Entry<String, String> entry : continueValues.entrySet()) {
urlBuilder.addQueryParameter(entry.getKey(), entry.getValue());
}
}
}
/**
* Append parameters for category image generator
*
* @param categoryName name of the category
* @param urlBuilder HttpUrl builder
*/
private void appendCategoryParams(String categoryName, HttpUrl.Builder urlBuilder) {
urlBuilder.addQueryParameter("generator", "categorymembers")
.addQueryParameter("gcmtype", "file")
.addQueryParameter("gcmtitle", categoryName)
.addQueryParameter("gcmsort", "timestamp")//property to sort by;timestamp
.addQueryParameter("gcmdir", "desc")//in which direction to sort;descending
.addQueryParameter("gcmlimit", "10");
}
/**
* Stores the continue values for action=query
* These values are sent to the server in the subsequent call to fetch results after this point
*
* @param keyword
* @param values
*/
private void putContinueValues(String keyword, Map<String, String> values) {
defaultKvStore.putJson("query_continue_" + keyword, values);
}
/**
* Retrieves a map of continue values from shared preferences.
* These values are appended to the next API call
*
* @param keyword
* @return
*/
private Map<String, String> getContinueValues(String keyword) {
return defaultKvStore.getJson("query_continue_" + keyword, mapType);
}
}

View file

@ -1,88 +0,0 @@
package fr.free.nrw.commons.mwapi;
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;
}
@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;
}
}

View file

@ -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;
}
}
}

View file

@ -0,0 +1,63 @@
package fr.free.nrw.commons.mwapi;
import java.util.Collections;
import javax.inject.Inject;
import io.reactivex.Observable;
import org.wikipedia.dataclient.mwapi.MwQueryLogEvent;
import org.wikipedia.dataclient.mwapi.MwQueryResponse;
import org.wikipedia.dataclient.mwapi.MwQueryResult;
import org.wikipedia.dataclient.mwapi.UserInfo;
import org.wikipedia.util.DateUtil;
import java.util.Date;
import javax.inject.Inject;
import io.reactivex.Single;
public class UserClient {
private final UserInterface userInterface;
@Inject
public UserClient(UserInterface userInterface) {
this.userInterface = userInterface;
}
/**
* Checks to see if a user is currently blocked from Commons
*
* @return whether or not the user is blocked from Commons
*/
public Single<Boolean> isUserBlockedFromCommons() {
return userInterface.getUserBlockInfo()
.map(MwQueryResponse::query)
.map(MwQueryResult::userInfo)
.map(UserInfo::blockexpiry)
.map(blockExpiry -> {
if (blockExpiry.isEmpty())
return false;
else if ("infinite".equals(blockExpiry))
return true;
else {
Date endDate = DateUtil.iso8601DateParse(blockExpiry);
Date current = new Date();
return endDate.after(current);
}
}).single(false);
}
public Observable<MwQueryLogEvent> logEvents(String user) {
try {
return userInterface.getUserLogEvents(user, Collections.emptyMap())
.map(MwQueryResponse::query)
.map(MwQueryResult::logevents)
.flatMap(Observable::fromIterable);
} catch (Throwable throwable) {
return Observable.empty();
}
}
}

View file

@ -0,0 +1,31 @@
package fr.free.nrw.commons.mwapi;
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;
import static org.wikipedia.dataclient.Service.MW_API_PREFIX;
public interface UserInterface {
/**
* Gets the log events of user
* @param user name of user without prefix
* @param continuation continuation params returned in previous query
* @return query response
*/
@GET(MW_API_PREFIX+"action=query&list=logevents&letype=upload&leprop=title|timestamp|ids&lelimit=500")
Observable<MwQueryResponse> getUserLogEvents(@Query("leuser") String user, @QueryMap Map<String, String> continuation);
/**
* Checks to see if a user is currently blocked from Commons
*/
@GET(MW_API_PREFIX + "action=query&meta=userinfo&uiprop=blockinfo")
Observable<MwQueryResponse> getUserBlockInfo();
}

View file

@ -1,5 +1,9 @@
package fr.free.nrw.commons.notification;
import org.wikipedia.util.DateUtil;
import fr.free.nrw.commons.utils.CommonsDateUtil;
/**
* Created by root on 18.12.2017.
*/
@ -8,33 +12,44 @@ public class Notification {
public NotificationType notificationType;
public String notificationText;
public String date;
public String description;
public String link;
public String iconUrl;
public String dateWithYear;
public String notificationId;
public Notification(NotificationType notificationType, String notificationText, String date, String description, String link, String iconUrl, String dateWithYear, String notificationId) {
public Notification(NotificationType notificationType,
String notificationText,
String date,
String link,
String iconUrl,
String notificationId) {
this.notificationType = notificationType;
this.notificationText = notificationText;
this.date = date;
this.description = description;
this.link = link;
this.iconUrl = iconUrl;
this.dateWithYear = dateWithYear;
this.notificationId=notificationId;
}
public static Notification from(org.wikipedia.notifications.Notification wikiNotification) {
org.wikipedia.notifications.Notification.Contents contents = wikiNotification.getContents();
String notificationLink = contents == null || contents.getLinks() == null
|| contents.getLinks().getPrimary() == null ? "" : contents.getLinks().getPrimary().getUrl();
return new Notification(NotificationType.UNKNOWN,
contents == null ? "" : contents.getCompactHeader(),
DateUtil.getMonthOnlyDateString(wikiNotification.getTimestamp()),
notificationLink,
"",
String.valueOf(wikiNotification.id()));
}
@Override
public String toString() {
return "Notification" +
"notificationType='" + notificationType + '\'' +
", notificationText='" + notificationText + '\'' +
", date='" + date + '\'' +
", description='" + description + '\'' +
", link='" + link + '\'' +
", iconUrl='" + iconUrl + '\'' +
", dateWithYear=" + dateWithYear +
", notificationId='" + notificationId + '\'' +
'}';
}

View file

@ -5,11 +5,6 @@ import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import androidx.constraintlayout.widget.ConstraintLayout;
import com.google.android.material.snackbar.Snackbar;
import androidx.recyclerview.widget.DividerItemDecoration;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
@ -19,10 +14,17 @@ import android.widget.RelativeLayout;
import android.widget.TextView;
import android.widget.Toast;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.recyclerview.widget.DividerItemDecoration;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.snackbar.Snackbar;
import com.pedrogomez.renderers.RVRendererAdapter;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Callable;
import javax.inject.Inject;
@ -34,7 +36,9 @@ import fr.free.nrw.commons.theme.NavigationBaseActivity;
import fr.free.nrw.commons.utils.NetworkUtils;
import fr.free.nrw.commons.utils.ViewUtil;
import io.reactivex.Observable;
import io.reactivex.ObservableSource;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
import timber.log.Timber;
@ -78,11 +82,12 @@ public class NotificationActivity extends NavigationBaseActivity {
@SuppressLint("CheckResult")
public void removeNotification(Notification notification) {
compositeDisposable.add(Observable.fromCallable(() -> controller.markAsRead(notification))
Disposable disposable = Observable.defer((Callable<ObservableSource<Boolean>>)
() -> controller.markAsRead(notification))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(result -> {
if (result){
if (result) {
notificationList.remove(notification);
setAdapter(notificationList);
adapter.notifyDataSetChanged();
@ -90,13 +95,12 @@ public class NotificationActivity extends NavigationBaseActivity {
.make(relativeLayout, getString(R.string.notification_mark_read), Snackbar.LENGTH_LONG);
snackbar.show();
if (notificationList.size()==0){
if (notificationList.size() == 0) {
setEmptyView();
relativeLayout.setVisibility(View.GONE);
no_notification.setVisibility(View.VISIBLE);
}
}
else {
} else {
adapter.notifyDataSetChanged();
setAdapter(notificationList);
Toast.makeText(NotificationActivity.this, getString(R.string.some_error), Toast.LENGTH_SHORT).show();
@ -107,7 +111,8 @@ public class NotificationActivity extends NavigationBaseActivity {
throwable.printStackTrace();
ViewUtil.showShortSnackbar(relativeLayout, R.string.error_notifications);
progressBar.setVisibility(View.GONE);
}));
});
compositeDisposable.add(disposable);
}
@ -140,11 +145,8 @@ public class NotificationActivity extends NavigationBaseActivity {
private void addNotifications(boolean archived) {
Timber.d("Add notifications");
if (mNotificationWorkerFragment == null) {
compositeDisposable.add(Observable.fromCallable(() -> {
progressBar.setVisibility(View.VISIBLE);
return controller.getNotifications(archived);
})
progressBar.setVisibility(View.VISIBLE);
compositeDisposable.add(controller.getNotifications(archived)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(notificationList -> {

View file

@ -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);
}
}
}

View file

@ -1,14 +1,12 @@
package fr.free.nrw.commons.notification;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Singleton;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import io.reactivex.Observable;
import io.reactivex.Single;
/**
* Created by root on 19.12.2017.
@ -16,27 +14,19 @@ import fr.free.nrw.commons.mwapi.MediaWikiApi;
@Singleton
public class NotificationController {
private MediaWikiApi mediaWikiApi;
private SessionManager sessionManager;
private NotificationClient notificationClient;
@Inject
public NotificationController(MediaWikiApi mediaWikiApi, SessionManager sessionManager) {
this.mediaWikiApi = mediaWikiApi;
this.sessionManager = sessionManager;
public NotificationController(NotificationClient notificationClient) {
this.notificationClient = notificationClient;
}
public List<Notification> getNotifications(boolean archived) throws IOException {
if (mediaWikiApi.validateLogin()) {
return mediaWikiApi.getNotifications(archived);
} else {
Boolean authTokenValidated = sessionManager.revalidateAuthToken();
if (authTokenValidated != null && authTokenValidated) {
return mediaWikiApi.getNotifications(archived);
}
}
return new ArrayList<>();
public Single<List<Notification>> getNotifications(boolean archived) {
return notificationClient.getNotifications(archived);
}
public boolean markAsRead(Notification notification) throws IOException{
return mediaWikiApi.markNotificationAsRead(notification);
Observable<Boolean> markAsRead(Notification notification) {
return notificationClient.markNotificationAsRead(notification.notificationId);
}
}

View file

@ -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;
}
}

View file

@ -47,7 +47,7 @@ public class QuizChecker {
/**
* constructor to set the parameters for quiz
* @param sessionManager
* @param okHttpJsonApiClient instance of MediaWikiApi
* @param okHttpJsonApiClient
*/
@Inject
public QuizChecker(SessionManager sessionManager,

View file

@ -11,8 +11,10 @@ import fr.free.nrw.commons.upload.UploadModel;
import fr.free.nrw.commons.upload.UploadModel.UploadItem;
import io.reactivex.Observable;
import io.reactivex.Single;
import java.util.Comparator;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Singleton;
@ -165,7 +167,7 @@ public class UploadRemoteDataSource {
}
/**
* ask the UploadModel for the image quality of the UploadItem
* ask the UplaodModel for the image quality of the UploadItem
*
* @param uploadItem
* @param shouldValidateTitle
@ -174,21 +176,4 @@ public class UploadRemoteDataSource {
public Single<Integer> getImageQuality(UploadItem uploadItem, boolean shouldValidateTitle) {
return uploadModel.getImageQuality(uploadItem, shouldValidateTitle);
}
/**
* Ask the CategoriesModel to search categories
* @param query
* @param imageTitleList
* @return
*/
public Observable<CategoryItem> searchCategories(String query, List<String> imageTitleList) {
return categoriesModel.searchCategories(query, imageTitleList);
}
/**
* Ask the CategoriesModel for default categories
*/
public Observable<CategoryItem> getDefaultCategories(List<String> imageTitleList) {
return categoriesModel.getDefaultCategories(imageTitleList);
}
}

View file

@ -8,8 +8,10 @@ import fr.free.nrw.commons.upload.SimilarImageInterface;
import fr.free.nrw.commons.upload.UploadModel.UploadItem;
import io.reactivex.Observable;
import io.reactivex.Single;
import java.util.Comparator;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Singleton;
@ -260,18 +262,4 @@ public class UploadRepository {
public void setSelectedLicense(String licenseName) {
localDataSource.setSelectedLicense(licenseName);
}
/**
* Ask the RemoteDataSource to search for categories
*/
public Observable<CategoryItem> searchCategories(String query, List<String> imageTitleList) {
return remoteDataSource.searchCategories(query, imageTitleList);
}
/**
* Ask the RemoteDataSource to get default categories
*/
public Observable<CategoryItem> getDefaultCategories(List<String> imageTitleList) {
return remoteDataSource.getDefaultCategories(imageTitleList);
}
}

View file

@ -28,16 +28,15 @@ import butterknife.BindView;
import butterknife.ButterKnife;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.auth.AuthenticatedActivity;
import fr.free.nrw.commons.delete.DeleteHelper;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.theme.NavigationBaseActivity;
import fr.free.nrw.commons.utils.DialogUtil;
import fr.free.nrw.commons.utils.ViewUtil;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.schedulers.Schedulers;
public class ReviewActivity extends AuthenticatedActivity {
public class ReviewActivity extends NavigationBaseActivity {
@BindView(R.id.pager_indicator_review)
public CirclePageIndicator pagerIndicator;
@ -60,8 +59,6 @@ public class ReviewActivity extends AuthenticatedActivity {
public ReviewPagerAdapter reviewPagerAdapter;
public ReviewController reviewController;
@Inject
MediaWikiApi mwApi;
@Inject
ReviewHelper reviewHelper;
@Inject
DeleteHelper deleteHelper;
@ -96,15 +93,6 @@ public class ReviewActivity extends AuthenticatedActivity {
return media;
}
@Override
protected void onAuthCookieAcquired(String authCookie) {
}
@Override
protected void onAuthFailure() {
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

View file

@ -4,8 +4,6 @@ import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.NotificationManager;
import android.content.Context;
import android.view.Gravity;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@ -13,17 +11,23 @@ import androidx.core.app.NotificationCompat;
import org.wikipedia.dataclient.mwapi.MwQueryPage;
import java.util.ArrayList;
import java.util.concurrent.Callable;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.actions.PageEditClient;
import fr.free.nrw.commons.actions.ThanksClient;
import fr.free.nrw.commons.delete.DeleteHelper;
import fr.free.nrw.commons.di.ApplicationlessInjection;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.utils.ViewUtil;
import io.reactivex.Observable;
import io.reactivex.ObservableSource;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
import timber.log.Timber;
@ -32,13 +36,15 @@ import timber.log.Timber;
public class ReviewController {
private static final int NOTIFICATION_SEND_THANK = 0x102;
private static final int NOTIFICATION_CHECK_CATEGORY = 0x101;
protected static ArrayList<String> categories;
@Inject
ThanksClient thanksClient;
private final DeleteHelper deleteHelper;
@Nullable
MwQueryPage.Revision firstRevision; // TODO: maybe we can expand this class to include fileName
@Inject
MediaWikiApi mwApi;
@Inject
SessionManager sessionManager;
@Named("commons-page-edit")
PageEditClient pageEditClient;
private NotificationManager notificationManager;
private NotificationCompat.Builder notificationBuilder;
private Media media;
@ -89,40 +95,16 @@ public class ReviewController {
.getCommonsApplicationComponent()
.inject(this);
Toast toast = new Toast(context);
toast.setGravity(Gravity.CENTER, 0, 0);
toast = Toast.makeText(context, context.getString(R.string.check_category_toast, media.getDisplayTitle()), Toast.LENGTH_SHORT);
toast.show();
ViewUtil.showShortToast(context, context.getString(R.string.check_category_toast, media.getDisplayTitle()));
Observable.fromCallable(() -> {
publishProgress(context, 0);
String editToken;
String authCookie;
String summary = context.getString(R.string.check_category_edit_summary);
authCookie = sessionManager.getAuthCookie();
mwApi.setAuthCookie(authCookie);
try {
editToken = mwApi.getEditToken();
if (editToken == null) {
return false;
}
publishProgress(context, 1);
mwApi.appendEdit(editToken, "\n{{subst:chc}}\n", media.getFilename(), summary);
publishProgress(context, 2);
} catch (Exception e) {
Timber.d(e);
return false;
}
return true;
})
publishProgress(context, 0);
String summary = context.getString(R.string.check_category_edit_summary);
Observable.defer((Callable<ObservableSource<Boolean>>) () ->
pageEditClient.appendEdit(media.getFilename(), "\n{{subst:chc}}\n", summary))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe((result) -> {
publishProgress(context, 2);
String message;
String title;
@ -136,15 +118,7 @@ public class ReviewController {
reviewCallback.onFailure();
}
notificationBuilder.setDefaults(NotificationCompat.DEFAULT_ALL)
.setContentTitle(title)
.setStyle(new NotificationCompat.BigTextStyle()
.bigText(message))
.setSmallIcon(R.drawable.ic_launcher)
.setProgress(0, 0, false)
.setOngoing(false)
.setPriority(NotificationCompat.PRIORITY_HIGH);
notificationManager.notify(NOTIFICATION_CHECK_CATEGORY, notificationBuilder.build());
showNotification(title, message);
}, Timber::e);
}
@ -172,39 +146,20 @@ public class ReviewController {
.getInstance(context)
.getCommonsApplicationComponent()
.inject(this);
Toast toast = new Toast(context);
toast.setGravity(Gravity.CENTER, 0, 0);
toast = Toast.makeText(context, context.getString(R.string.send_thank_toast, media.getDisplayTitle()), Toast.LENGTH_SHORT);
toast.show();
ViewUtil.showShortToast(context, context.getString(R.string.send_thank_toast, media.getDisplayTitle()));
Observable.fromCallable(() -> {
publishProgress(context, 0);
publishProgress(context, 0);
if (firstRevision == null) {
return;
}
String editToken;
String authCookie;
authCookie = sessionManager.getAuthCookie();
mwApi.setAuthCookie(authCookie);
try {
editToken = mwApi.getEditToken();
if (editToken == null) {
return false;
}
publishProgress(context, 1);
assert firstRevision != null;
mwApi.thank(editToken, firstRevision.getRevisionId());
publishProgress(context, 2);
} catch (Exception e) {
Timber.d(e);
return false;
}
return true;
})
Observable.defer((Callable<ObservableSource<Boolean>>) () -> thanksClient.thank(firstRevision.getRevisionId()))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe((result) -> {
String message = "";
String title = "";
publishProgress(context, 2);
String message;
String title;
if (result) {
title = context.getString(R.string.send_thank_success_title);
message = context.getString(R.string.send_thank_success_message, media.getDisplayTitle());
@ -213,19 +168,23 @@ public class ReviewController {
message = context.getString(R.string.send_thank_failure_message, media.getDisplayTitle());
}
notificationBuilder.setDefaults(NotificationCompat.DEFAULT_ALL)
.setContentTitle(title)
.setStyle(new NotificationCompat.BigTextStyle()
.bigText(message))
.setSmallIcon(R.drawable.ic_launcher)
.setProgress(0, 0, false)
.setOngoing(false)
.setPriority(NotificationCompat.PRIORITY_HIGH);
notificationManager.notify(NOTIFICATION_SEND_THANK, notificationBuilder.build());
showNotification(title, message);
}, Timber::e);
}
private void showNotification(String title, String message) {
notificationBuilder.setDefaults(NotificationCompat.DEFAULT_ALL)
.setContentTitle(title)
.setStyle(new NotificationCompat.BigTextStyle()
.bigText(message))
.setSmallIcon(R.drawable.ic_launcher)
.setProgress(0, 0, false)
.setOngoing(false)
.setPriority(NotificationCompat.PRIORITY_HIGH);
notificationManager.notify(NOTIFICATION_SEND_THANK, notificationBuilder.build());
}
public interface ReviewCallback {
void onSuccess();

View file

@ -14,8 +14,7 @@ import javax.inject.Inject;
import javax.inject.Singleton;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
import fr.free.nrw.commons.media.MediaClient;
import io.reactivex.Observable;
import io.reactivex.Single;
@ -24,16 +23,12 @@ public class ReviewHelper {
private static final String[] imageExtensions = new String[]{".jpg", ".jpeg", ".png"};
private final OkHttpJsonApiClient okHttpJsonApiClient;
private final MediaWikiApi mediaWikiApi;
private final MediaClient mediaClient;
private final ReviewInterface reviewInterface;
@Inject
public ReviewHelper(OkHttpJsonApiClient okHttpJsonApiClient,
MediaWikiApi mediaWikiApi,
ReviewInterface reviewInterface) {
this.okHttpJsonApiClient = okHttpJsonApiClient;
this.mediaWikiApi = mediaWikiApi;
public ReviewHelper(MediaClient mediaClient, ReviewInterface reviewInterface) {
this.mediaClient = mediaClient;
this.reviewInterface = reviewInterface;
}
@ -42,6 +37,7 @@ public class ReviewHelper {
* Calls the API to get 10 changes in the last 1 hour
* Earlier we were getting changes for the last 30 days but as the API returns just 10 results
* its best to fetch for just last 1 hour.
*
* @return
*/
private Observable<RecentChange> getRecentChanges() {
@ -81,23 +77,25 @@ public class ReviewHelper {
/**
* Returns a proper Media object if the file is not already nominated for deletion
* Else it returns an empty Media object
*
* @param recentChange
* @return
*/
private Single<Media> getRandomMediaFromRecentChange(RecentChange recentChange) {
return Single.just(recentChange)
.flatMap(change -> mediaWikiApi.pageExists("Commons:Deletion_requests/" + change.getTitle()))
.flatMap(change -> mediaClient.checkPageExistsUsingTitle("Commons:Deletion_requests/" + change.getTitle()))
.flatMap(isDeleted -> {
if (isDeleted) {
return Single.just(new Media(""));
}
return okHttpJsonApiClient.getMedia(recentChange.getTitle(), false);
return mediaClient.getMedia(recentChange.getTitle());
});
}
/**
* Gets the first revision of the file from filename
*
* @param filename
* @return
*/
@ -110,6 +108,7 @@ public class ReviewHelper {
* Checks if the change is reviewable or not.
* - checks the type and revisionId of the change
* - checks supported image extensions
*
* @param recentChange
* @return
*/

View file

@ -1,12 +1,15 @@
package fr.free.nrw.commons.settings;
import android.Manifest;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Bundle;
import android.preference.EditTextPreference;
import android.preference.MultiSelectListPreference;
import android.preference.ListPreference;
import android.preference.Preference;
import android.preference.PreferenceFragment;
import android.preference.PreferenceManager;
import android.preference.SwitchPreference;
import android.text.Editable;
import android.text.TextWatcher;
@ -28,7 +31,6 @@ import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.di.ApplicationlessInjection;
import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.logging.CommonsLogSender;
import fr.free.nrw.commons.ui.LongTitlePreferences.LongTitleMultiSelectListPreference;
import fr.free.nrw.commons.utils.PermissionUtils;
import fr.free.nrw.commons.utils.ViewUtil;
import fr.free.nrw.commons.upload.Language;
@ -68,9 +70,8 @@ public class SettingsFragment extends PreferenceFragment {
return true;
});
LongTitleMultiSelectListPreference multiSelectListPref = (LongTitleMultiSelectListPreference) findPreference("manageExifTags");
MultiSelectListPreference multiSelectListPref = (MultiSelectListPreference) findPreference("manageExifTags");
if (multiSelectListPref != null) {
defaultKvStore.putJson(Prefs.MANAGED_EXIF_TAGS, multiSelectListPref.getValues());
multiSelectListPref.setOnPreferenceChangeListener((preference, newValue) -> {
defaultKvStore.putJson(Prefs.MANAGED_EXIF_TAGS, newValue);
return true;

View file

@ -1,8 +1,11 @@
package fr.free.nrw.commons.theme;
import static fr.free.nrw.commons.di.NetworkingModule.NAMED_COMMONS_WIKI_SITE;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.app.ActivityManager;
import android.app.ProgressDialog;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
@ -22,6 +25,10 @@ import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import fr.free.nrw.commons.auth.LogoutClient;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.schedulers.Schedulers;
import javax.inject.Inject;
import javax.inject.Named;
@ -40,6 +47,8 @@ import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.logging.CommonsLogSender;
import fr.free.nrw.commons.review.ReviewActivity;
import fr.free.nrw.commons.settings.SettingsActivity;
import org.wikipedia.dataclient.Service;
import org.wikipedia.dataclient.WikiSite;
import timber.log.Timber;
public abstract class NavigationBaseActivity extends BaseActivity
@ -61,6 +70,15 @@ public abstract class NavigationBaseActivity extends BaseActivity
private ActionBarDrawerToggle toggle;
@Inject
LogoutClient logoutClient;
private CompositeDisposable disposable = new CompositeDisposable();
private Service service;
private ProgressDialog progressDialog;
public void initDrawer() {
navigationView.setNavigationItemSelectedListener(this);
@ -201,9 +219,7 @@ public abstract class NavigationBaseActivity extends BaseActivity
.setMessage(R.string.logout_verification)
.setCancelable(false)
.setPositiveButton(R.string.yes, (dialog, which) -> {
BaseLogoutListener logoutListener = new BaseLogoutListener();
CommonsApplication app = (CommonsApplication) getApplication();
app.clearApplicationData(this, logoutListener);
handleLogout();
})
.setNegativeButton(R.string.no, (dialog, which) -> dialog.cancel())
.show();
@ -227,6 +243,36 @@ public abstract class NavigationBaseActivity extends BaseActivity
}
}
/**
* Ask the logout client to post the logout api
*/
private void handleLogout() {
if (null == progressDialog) {
progressDialog = new ProgressDialog(this);
progressDialog.setMessage(getString(R.string.please_wait));
}
progressDialog.show();
disposable.add(logoutClient.postLogout()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(mwQueryResponse -> {
BaseLogoutListener logoutListener = new BaseLogoutListener();
CommonsApplication app = (CommonsApplication) getApplication();
app.clearApplicationData(this, logoutListener);
progressDialog.cancel();
},
t -> {
progressDialog.cancel();
Toast.makeText(NavigationBaseActivity.this,
t.getLocalizedMessage(), Toast.LENGTH_SHORT).show();
Timber.e(t, "Something went wrong with post logout api: %s", t
.getLocalizedMessage());
}
));
}
private class BaseLogoutListener implements CommonsApplication.LogoutListener {
@Override
public void onLogoutComplete() {
@ -294,4 +340,13 @@ public abstract class NavigationBaseActivity extends BaseActivity
super.onBackPressed();
}
}
@Override
protected void onStop() {
super.onStop();
disposable.clear();
if (progressDialog != null && progressDialog.isShowing()) {
progressDialog.cancel();
}
}
}

View file

@ -0,0 +1,86 @@
package fr.free.nrw.commons.upload;
import java.io.IOException;
import okhttp3.MediaType;
import okhttp3.RequestBody;
import okio.Buffer;
import okio.BufferedSink;
import okio.ForwardingSink;
import okio.Okio;
import okio.Sink;
/**
* Decorates an OkHttp request body to count the number of bytes written when writing it. Can
* decorate any request body, but is most useful for tracking the upload progress of large multipart
* requests.
*
* @author Ashish Kumar
*/
public class CountingRequestBody extends RequestBody {
protected RequestBody delegate;
protected Listener listener;
protected CountingSink countingSink;
public CountingRequestBody(RequestBody delegate, Listener listener) {
this.delegate = delegate;
this.listener = listener;
}
@Override
public MediaType contentType() {
return delegate.contentType();
}
@Override
public long contentLength() {
try {
return delegate.contentLength();
} catch (IOException e) {
e.printStackTrace();
}
return -1;
}
@Override
public void writeTo(BufferedSink sink) throws IOException {
countingSink = new CountingSink(sink);
BufferedSink bufferedSink = Okio.buffer(countingSink);
delegate.writeTo(bufferedSink);
bufferedSink.flush();
}
protected final class CountingSink extends ForwardingSink {
private long bytesWritten = 0;
public CountingSink(Sink delegate) {
super(delegate);
}
@Override
public void write(Buffer source, long byteCount) throws IOException {
super.write(source, byteCount);
bytesWritten += byteCount;
listener.onRequestProgress(bytesWritten, contentLength());
}
}
public interface Listener {
/**
* Will be triggered when write progresses
* @param bytesWritten
* @param contentLength
*/
void onRequestProgress(long bytesWritten, long contentLength);
}
}

View file

@ -105,13 +105,13 @@ public class FileProcessor implements Callback {
*/
private Set<String> getExifTagsToRedact(Context context) {
Type setType = new TypeToken<Set<String>>() {}.getType();
Set<String> selectedExifTags = defaultKvStore.getJson(Prefs.MANAGED_EXIF_TAGS, setType);
Set<String> prefManageEXIFTags = defaultKvStore.getJson(Prefs.MANAGED_EXIF_TAGS, setType);
Set<String> redactTags = new HashSet<>(Arrays.asList(
context.getResources().getStringArray(R.array.pref_exifTag_values)));
Timber.d(redactTags.toString());
if (selectedExifTags != null) redactTags.removeAll(selectedExifTags);
else redactTags.clear();
if (prefManageEXIFTags != null) redactTags.removeAll(prefManageEXIFTags);
return redactTags;
}

View file

@ -140,7 +140,7 @@ public class FileUtils {
return mimeType;
}
public static String getFileExt(String fileName) {
static String getFileExt(String fileName) {
//Default filePath extension
String extension = ".jpg";
@ -151,7 +151,7 @@ public class FileUtils {
return extension;
}
public static FileInputStream getFileInputStream(String filePath) throws FileNotFoundException {
static FileInputStream getFileInputStream(String filePath) throws FileNotFoundException {
return new FileInputStream(filePath);
}

View file

@ -1,11 +1,13 @@
package fr.free.nrw.commons.upload;
import android.content.Context;
import org.apache.commons.lang3.StringUtils;
import javax.inject.Inject;
import javax.inject.Singleton;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.media.MediaClient;
import fr.free.nrw.commons.nearby.Place;
import fr.free.nrw.commons.utils.ImageUtils;
import fr.free.nrw.commons.utils.ImageUtilsWrapper;
@ -23,19 +25,22 @@ import static fr.free.nrw.commons.utils.ImageUtils.IMAGE_OK;
public class ImageProcessingService {
private final FileUtilsWrapper fileUtilsWrapper;
private final ImageUtilsWrapper imageUtilsWrapper;
private final MediaWikiApi mwApi;
private final ReadFBMD readFBMD;
private final EXIFReader EXIFReader;
private final MediaClient mediaClient;
private final Context context;
@Inject
public ImageProcessingService(FileUtilsWrapper fileUtilsWrapper,
ImageUtilsWrapper imageUtilsWrapper,
MediaWikiApi mwApi, ReadFBMD readFBMD, EXIFReader EXIFReader) {
ReadFBMD readFBMD, EXIFReader EXIFReader,
MediaClient mediaClient, Context context) {
this.fileUtilsWrapper = fileUtilsWrapper;
this.imageUtilsWrapper = imageUtilsWrapper;
this.mwApi = mwApi;
this.readFBMD = readFBMD;
this.EXIFReader = EXIFReader;
this.mediaClient = mediaClient;
this.context = context;
}
/**
@ -106,7 +111,7 @@ public class ImageProcessingService {
return Single.just(EMPTY_TITLE);
}
return Single.fromCallable(() -> mwApi.fileExistsWithName(uploadItem.getFileName()))
return mediaClient.checkPageExistsUsingTitle("File:" + uploadItem.getFileName())
.map(doesFileExist -> {
Timber.d("Result for valid title is %s", doesFileExist);
return doesFileExist ? FILE_NAME_EXISTS : IMAGE_OK;
@ -124,7 +129,7 @@ public class ImageProcessingService {
return Single.fromCallable(() ->
fileUtilsWrapper.getFileInputStream(filePath))
.map(fileUtilsWrapper::getSHA1)
.map(mwApi::existingFile)
.flatMap(mediaClient::checkFileExistsUsingSha)
.map(b -> {
Timber.d("Result for duplicate image %s", b);
return b ? ImageUtils.IMAGE_DUPLICATE : ImageUtils.IMAGE_OK;

View file

@ -9,6 +9,7 @@ import android.annotation.SuppressLint;
import android.app.ProgressDialog;
import android.content.Intent;
import android.os.Bundle;
import androidx.appcompat.app.AlertDialog;
import androidx.cardview.widget.CardView;
import androidx.fragment.app.Fragment;
@ -25,6 +26,7 @@ import android.widget.ImageButton;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.List;
@ -43,6 +45,7 @@ import fr.free.nrw.commons.contributions.Contribution;
import fr.free.nrw.commons.contributions.ContributionController;
import fr.free.nrw.commons.filepicker.UploadableFile;
import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.mwapi.UserClient;
import fr.free.nrw.commons.nearby.Place;
import fr.free.nrw.commons.theme.BaseActivity;
import fr.free.nrw.commons.upload.categories.UploadCategoriesFragment;
@ -51,17 +54,29 @@ import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment;
import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment.UploadMediaDetailFragmentCallback;
import fr.free.nrw.commons.utils.PermissionUtils;
import fr.free.nrw.commons.utils.ViewUtil;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import java.util.Collections;
import io.reactivex.schedulers.Schedulers;
import timber.log.Timber;
public class UploadActivity extends BaseActivity implements UploadContract.View ,UploadBaseFragment.Callback{
public class UploadActivity extends BaseActivity implements UploadContract.View, UploadBaseFragment.Callback {
@Inject
ContributionController contributionController;
@Inject @Named("default_preferences") JsonKvStore directKvStore;
@Inject UploadContract.UserActionListener presenter;
@Inject CategoriesModel categoriesModel;
@Inject SessionManager sessionManager;
@Inject
@Named("default_preferences")
JsonKvStore directKvStore;
@Inject
UploadContract.UserActionListener presenter;
@Inject
CategoriesModel categoriesModel;
@Inject
SessionManager sessionManager;
@Inject
UserClient userClient;
@BindView(R.id.cv_container_top_card)
CardView cvContainerTopCard;
@ -84,7 +99,7 @@ public class UploadActivity extends BaseActivity implements UploadContract.View
@BindView(R.id.vp_upload)
ViewPager vpUpload;
private boolean isTitleExpanded=true;
private boolean isTitleExpanded = true;
private CompositeDisposable compositeDisposable;
private ProgressDialog progressDialog;
@ -97,8 +112,8 @@ public class UploadActivity extends BaseActivity implements UploadContract.View
private String source;
private Place place;
private List<UploadableFile> uploadableFiles= Collections.emptyList();
private int currentSelectedPosition=0;
private List<UploadableFile> uploadableFiles = Collections.emptyList();
private int currentSelectedPosition = 0;
@SuppressLint("CheckResult")
@Override
@ -133,24 +148,24 @@ public class UploadActivity extends BaseActivity implements UploadContract.View
private void initThumbnailsRecyclerView() {
rvThumbnails.setLayoutManager(new LinearLayoutManager(this,
LinearLayoutManager.HORIZONTAL, false));
thumbnailsAdapter=new ThumbnailsAdapter(() -> currentSelectedPosition);
thumbnailsAdapter = new ThumbnailsAdapter(() -> currentSelectedPosition);
rvThumbnails.setAdapter(thumbnailsAdapter);
}
private void initViewPager() {
uploadImagesAdapter=new UploadImageAdapter(getSupportFragmentManager());
uploadImagesAdapter = new UploadImageAdapter(getSupportFragmentManager());
vpUpload.setAdapter(uploadImagesAdapter);
vpUpload.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
@Override
public void onPageScrolled(int position, float positionOffset,
int positionOffsetPixels) {
int positionOffsetPixels) {
}
@Override
public void onPageSelected(int position) {
currentSelectedPosition=position;
currentSelectedPosition = position;
if (position >= uploadableFiles.size()) {
cvContainerTopCard.setVisibility(View.GONE);
} else {
@ -179,9 +194,24 @@ public class UploadActivity extends BaseActivity implements UploadContract.View
if (!isLoggedIn()) {
askUserToLogIn();
}
checkBlockStatus();
checkStoragePermissions();
}
/**
* Makes API call to check if user is blocked from Commons. If the user is blocked, a snackbar
* is created to notify the user
*/
protected void checkBlockStatus() {
compositeDisposable.add(userClient.isUserBlockedFromCommons()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.filter(result -> result)
.subscribe(result -> showInfoAlert(R.string.block_notification_title,
R.string.block_notification, UploadActivity.this::finish)
));
}
private void checkStoragePermissions() {
PermissionUtils.checkPermissionsAndPerformAction(this,
Manifest.permission.WRITE_EXTERNAL_STORAGE,
@ -236,7 +266,7 @@ public class UploadActivity extends BaseActivity implements UploadContract.View
@Override
public void showHideTopCard(boolean shouldShow) {
llContainerTopCard.setVisibility(shouldShow?View.VISIBLE:View.GONE);
llContainerTopCard.setVisibility(shouldShow ? View.VISIBLE : View.GONE);
}
@Override
@ -290,13 +320,13 @@ public class UploadActivity extends BaseActivity implements UploadContract.View
llContainerTopCard.setVisibility(View.GONE);
}
tvTopCardTitle.setText(getResources()
.getQuantityString(R.plurals.upload_count_title, uploadableFiles.size(),uploadableFiles.size()));
.getQuantityString(R.plurals.upload_count_title, uploadableFiles.size(), uploadableFiles.size()));
fragments = new ArrayList<>();
for (UploadableFile uploadableFile : uploadableFiles) {
UploadMediaDetailFragment uploadMediaDetailFragment = new UploadMediaDetailFragment();
uploadMediaDetailFragment.setImageTobeUploaded(uploadableFile, source, place);
uploadMediaDetailFragment.setCallback(new UploadMediaDetailFragmentCallback(){
uploadMediaDetailFragment.setCallback(new UploadMediaDetailFragmentCallback() {
@Override
public void deletePictureAtIndex(int index) {
presenter.deletePictureAtIndex(index);
@ -383,19 +413,22 @@ public class UploadActivity extends BaseActivity implements UploadContract.View
finish();
}
private void showInfoAlert(int titleStringID, int messageStringId, String... formatArgs) {
private void showInfoAlert(int titleStringID, int messageStringId, Runnable positive, String... formatArgs) {
new AlertDialog.Builder(this)
.setTitle(titleStringID)
.setMessage(getString(messageStringId, (Object[]) formatArgs))
.setCancelable(true)
.setPositiveButton(android.R.string.ok, (dialog, id) -> dialog.cancel())
.setPositiveButton(android.R.string.ok, (dialog, id) -> {
positive.run();
dialog.cancel();
})
.create()
.show();
}
@Override
public void onNextButtonClicked(int index) {
if (index < fragments.size()-1) {
if (index < fragments.size() - 1) {
vpUpload.setCurrentItem(index + 1, false);
} else {
presenter.handleSubmit();
@ -426,23 +459,25 @@ public class UploadActivity extends BaseActivity implements UploadContract.View
notifyDataSetChanged();
}
@Override public Fragment getItem(int position) {
@Override
public Fragment getItem(int position) {
return fragments.get(position);
}
@Override public int getCount() {
@Override
public int getCount() {
return fragments.size();
}
@Override
public int getItemPosition(Object object){
public int getItemPosition(Object object) {
return PagerAdapter.POSITION_NONE;
}
}
@OnClick(R.id.rl_container_title)
public void onRlContainerTitleClicked(){
public void onRlContainerTitleClicked() {
rvThumbnails.setVisibility(isTitleExpanded ? View.GONE : View.VISIBLE);
isTitleExpanded = !isTitleExpanded;
ibToggleTopCard.setRotation(ibToggleTopCard.getRotation() + 180);

View file

@ -0,0 +1,69 @@
package fr.free.nrw.commons.upload;
import static fr.free.nrw.commons.di.NetworkingModule.NAMED_COMMONS_CSRF;
import android.content.Context;
import android.net.Uri;
import fr.free.nrw.commons.contributions.Contribution;
import fr.free.nrw.commons.upload.UploadService.NotificationUpdateProgressListener;
import io.reactivex.Observable;
import java.io.File;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import okhttp3.MediaType;
import okhttp3.MultipartBody;
import okhttp3.RequestBody;
import org.wikipedia.csrf.CsrfTokenClient;
@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,
NotificationUpdateProgressListener notificationUpdater) {
RequestBody requestBody = RequestBody
.create(MediaType.parse(FileUtils.getMimeType(context, Uri.parse(file.getPath()))), file);
CountingRequestBody countingRequestBody = new CountingRequestBody(requestBody,
(bytesWritten, contentLength) -> notificationUpdater
.onProgress(bytesWritten, contentLength));
MultipartBody.Part filePart = MultipartBody.Part.createFormData("file", filename, countingRequestBody);
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);
}
}
}

View file

@ -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);
}

View file

@ -264,7 +264,6 @@ public class UploadModel {
this.createdTimestampSource = createdTimestampSource;
title = new Title();
descriptions = new ArrayList<>();
descriptions.add(new Description());
this.place = place;
this.mediaUri = mediaUri;
this.mimeType = mimeType;

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -10,20 +10,6 @@ import android.net.Uri;
import android.os.Bundle;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import android.widget.Toast;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashSet;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.inject.Inject;
import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.HandlerService;
@ -33,10 +19,18 @@ import fr.free.nrw.commons.contributions.Contribution;
import fr.free.nrw.commons.contributions.ContributionDao;
import fr.free.nrw.commons.contributions.ContributionsContentProvider;
import fr.free.nrw.commons.contributions.MainActivity;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.media.MediaClient;
import fr.free.nrw.commons.utils.CommonsDateUtil;
import fr.free.nrw.commons.wikidata.WikidataEditService;
import io.reactivex.Single;
import io.reactivex.Observable;
import io.reactivex.schedulers.Schedulers;
import java.io.File;
import java.io.IOException;
import java.util.HashSet;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.inject.Inject;
import timber.log.Timber;
public class UploadService extends HandlerService<Contribution> {
@ -50,10 +44,11 @@ public class UploadService extends HandlerService<Contribution> {
public static final String EXTRA_FILES = EXTRA_PREFIX + ".files";
public static final String EXTRA_CAMPAIGN = EXTRA_PREFIX + ".campaign";
@Inject MediaWikiApi mwApi;
@Inject WikidataEditService wikidataEditService;
@Inject SessionManager sessionManager;
@Inject ContributionDao contributionDao;
@Inject UploadClient uploadClient;
@Inject MediaClient mediaClient;
private NotificationManagerCompat notificationManager;
private NotificationCompat.Builder curNotification;
@ -75,7 +70,7 @@ public class UploadService extends HandlerService<Contribution> {
super("UploadService");
}
private class NotificationUpdateProgressListener implements MediaWikiApi.ProgressListener {
protected class NotificationUpdateProgressListener{
String notificationTag;
boolean notificationTitleChanged;
@ -91,7 +86,6 @@ public class UploadService extends HandlerService<Contribution> {
this.contribution = contribution;
}
@Override
public void onProgress(long transferred, long total) {
Timber.d("Uploaded %d of %d", transferred, total);
if (!notificationTitleChanged) {
@ -197,29 +191,20 @@ public class UploadService extends HandlerService<Contribution> {
@SuppressLint("CheckResult")
private void uploadContribution(Contribution contribution) {
InputStream fileInputStream;
Uri localUri = contribution.getLocalUri();
if (localUri == null || localUri.getPath() == null) {
Timber.d("localUri/path is null");
return;
}
String notificationTag = localUri.toString();
try {
File file1 = new File(localUri.getPath());
fileInputStream = new FileInputStream(file1);
} catch (FileNotFoundException e) {
Timber.d("File not found");
Toast fileNotFound = Toast.makeText(this, R.string.upload_failed, Toast.LENGTH_LONG);
fileNotFound.show();
return;
}
File localFile = new File(localUri.getPath());
Timber.d("Before execution!");
curNotification.setContentTitle(getString(R.string.upload_progress_notification_title_start, contribution.getDisplayTitle()))
.setContentText(getResources().getQuantityString(R.plurals.uploads_pending_notification_indicator, toUpload, toUpload))
.setTicker(getString(R.string.upload_progress_notification_title_in_progress, contribution.getDisplayTitle()));
notificationManager.notify(notificationTag, NOTIFICATION_UPLOAD_IN_PROGRESS, curNotification.build());
notificationManager
.notify(notificationTag, NOTIFICATION_UPLOAD_IN_PROGRESS, curNotification.build());
String filename = contribution.getFilename();
@ -229,22 +214,10 @@ public class UploadService extends HandlerService<Contribution> {
contribution
);
Single.fromCallable(() -> {
if (!mwApi.validateLogin()) {
// Need to revalidate!
if (sessionManager.revalidateAuthToken()) {
Timber.d("Successfully revalidated token!");
} else {
Timber.d("Unable to revalidate :(");
stopForeground(true);
sessionManager.forceLogin(UploadService.this);
throw new RuntimeException(getString(R.string.authentication_failed));
}
}
return "Temp_" + contribution.hashCode() + filename;
}).flatMap(stashFilename -> mwApi.uploadFile(
stashFilename, fileInputStream, contribution.getDataLength(),
localUri, contribution.getContentProviderUri(), notificationUpdater))
Observable.fromCallable(() -> "Temp_" + contribution.hashCode() + filename)
.flatMap(stashFilename -> uploadClient
.uploadFileToStash(getApplicationContext(), stashFilename, localFile,
notificationUpdater))
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.io())
.doFinally(() -> {
@ -259,26 +232,24 @@ public class UploadService extends HandlerService<Contribution> {
}
})
.flatMap(uploadStash -> {
notificationManager.cancel(NOTIFICATION_UPLOAD_IN_PROGRESS);
notificationManager.cancel(notificationTag, NOTIFICATION_UPLOAD_IN_PROGRESS);
Timber.d("Stash upload response 1 is %s", uploadStash.toString());
String resultStatus = uploadStash.getResultStatus();
String resultStatus = uploadStash.getResult();
if (!resultStatus.equals("Success")) {
Timber.d("Contribution upload failed. Wikidata entity won't be edited");
showFailedNotification(contribution);
return Single.never();
return Observable.never();
} else {
synchronized (unfinishedUploads) {
Timber.d("making sure of uniqueness of name: %s", filename);
String uniqueFilename = findUniqueFilename(filename);
unfinishedUploads.add(uniqueFilename);
return mwApi.uploadFileFinalize(
uniqueFilename,
uploadStash.getFilekey(),
contribution.getPageContents(getApplicationContext()),
contribution.getEditSummary());
}
Timber.d("making sure of uniqueness of name: %s", filename);
String uniqueFilename = findUniqueFilename(filename);
unfinishedUploads.add(uniqueFilename);
return uploadClient.uploadFileFromStash(
getApplicationContext(),
contribution,
uniqueFilename,
uploadStash.getFilekey());
}
})
.subscribe(uploadResult -> {
@ -286,24 +257,25 @@ public class UploadService extends HandlerService<Contribution> {
notificationManager.cancel(notificationTag, NOTIFICATION_UPLOAD_IN_PROGRESS);
String resultStatus = uploadResult.getResultStatus();
String resultStatus = uploadResult.getResult();
if (!resultStatus.equals("Success")) {
Timber.d("Contribution upload failed. Wikidata entity won't be edited");
showFailedNotification(contribution);
} else {
String canonicalFilename = uploadResult.getCanonicalFilename();
String canonicalFilename = "File:" + uploadResult.getFilename();
Timber.d("Contribution upload success. Initiating Wikidata edit for entity id %s",
contribution.getWikiDataEntityId());
wikidataEditService.createClaimWithLogging(contribution.getWikiDataEntityId(), canonicalFilename);
contribution.setFilename(canonicalFilename);
contribution.setImageUrl(uploadResult.getImageUrl());
contribution.setImageUrl(uploadResult.getImageinfo().getOriginalUrl());
contribution.setState(Contribution.STATE_COMPLETED);
contribution.setDateUploaded(uploadResult.getDateUploaded());
contribution.setDateUploaded(CommonsDateUtil.getIso8601DateFormatShort()
.parse(uploadResult.getImageinfo().getTimestamp()));
contributionDao.save(contribution);
}
}, throwable -> {
Timber.w(throwable, "Exception during upload");
notificationManager.cancel(NOTIFICATION_UPLOAD_IN_PROGRESS);
notificationManager.cancel(notificationTag, NOTIFICATION_UPLOAD_IN_PROGRESS);
showFailedNotification(contribution);
});
}
@ -337,7 +309,7 @@ public class UploadService extends HandlerService<Contribution> {
sequenceFileName = regexMatcher.replaceAll("$1 " + sequenceNumber + "$2");
}
}
if (!mwApi.fileExistsWithName(sequenceFileName)
if (!mediaClient.checkPageExistsUsingTitle(String.format("File:%s",sequenceFileName)).blockingGet()
&& !unfinishedUploads.contains(sequenceFileName)) {
break;
}

View file

@ -80,9 +80,6 @@ public class CategoriesPresenter implements CategoriesContract.UserActionListene
.observeOn(ioScheduler)
.concatWith(
repository.searchAll(query, imageTitleList)
.mergeWith(repository.searchCategories(query, imageTitleList))
.concatWith(TextUtils.isEmpty(query) ? repository
.getDefaultCategories(imageTitleList) : Observable.empty())
)
.filter(categoryItem -> !repository.containsYear(categoryItem.getName()))
.distinct();

View file

@ -89,7 +89,7 @@ public class UploadCategoriesFragment extends UploadBaseFragment implements Cate
@Override
public void onResume() {
super.onResume();
if (presenter != null && isVisible) {
if (presenter != null && isVisible && (categories == null || categories.isEmpty())) {
presenter.searchForCategories(null);
}
}
@ -193,7 +193,7 @@ public class UploadCategoriesFragment extends UploadBaseFragment implements Cate
super.setUserVisibleHint(isVisibleToUser);
isVisible = isVisibleToUser;
if (presenter != null && isResumed()) {
if (presenter != null && isResumed() && (categories == null || categories.isEmpty())) {
presenter.searchForCategories(null);
}
}

View file

@ -4,8 +4,6 @@ import android.content.Context;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
@ -21,7 +19,6 @@ import androidx.appcompat.widget.AppCompatImageButton;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.github.chrisbanes.photoview.OnScaleChangedListener;
import com.github.chrisbanes.photoview.PhotoView;
import com.jakewharton.rxbinding2.widget.RxTextView;
@ -222,12 +219,20 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
* init the recycler veiw
*/
private void initRecyclerView() {
descriptionsAdapter = new DescriptionsAdapter(defaultKvStore.getString(Prefs.KEY_LANGUAGE_VALUE,""));
descriptionsAdapter = new DescriptionsAdapter(defaultKvStore.getString(Prefs.KEY_LANGUAGE_VALUE, ""));
descriptionsAdapter.setCallback(this::showInfoAlert);
rvDescriptions.setLayoutManager(new LinearLayoutManager(getContext()));
rvDescriptions.setAdapter(descriptionsAdapter);
}
/**
* returns the default locale value of the user's device
* @return
*/
private String getUserDefaultLocale() {
return getContext().getResources().getConfiguration().locale.getLanguage();
}
/**
* show dialog with info
* @param titleStringID
@ -395,16 +400,15 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
presenter.fetchPreviousTitleAndDescription(callback.getIndexInViewFlipper(this));
}
private void setDescriptionsInAdapter(List<Description> descriptions){
if(descriptions==null){
descriptions=new ArrayList<>();
private void setDescriptionsInAdapter(List<Description> descriptions) {
if (descriptions == null) {
descriptions = new ArrayList<>();
}
if(descriptions.size()==0){
descriptions.add(new Description());
if (descriptions.size() == 0) {
descriptionsAdapter.addDescription(new Description());
} else {
descriptionsAdapter.setItems(descriptions);
}
descriptionsAdapter.setItems(descriptions);
}
}

View file

@ -27,6 +27,7 @@ import androidx.annotation.Nullable;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.contributions.MainActivity;
import fr.free.nrw.commons.di.ApplicationlessInjection;
import fr.free.nrw.commons.media.MediaClient;
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
@ -42,7 +43,7 @@ public class PicOfDayAppWidget extends AppWidgetProvider {
private CompositeDisposable compositeDisposable = new CompositeDisposable();
@Inject OkHttpJsonApiClient okHttpJsonApiClient;
@Inject MediaClient mediaClient;
void updateAppWidget(Context context, AppWidgetManager appWidgetManager, int appWidgetId) {
RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.pic_of_day_app_widget);
@ -67,7 +68,7 @@ public class PicOfDayAppWidget extends AppWidgetProvider {
RemoteViews views,
AppWidgetManager appWidgetManager,
int appWidgetId) {
compositeDisposable.add(okHttpJsonApiClient.getPictureOfTheDay()
compositeDisposable.add(mediaClient.getPictureOfTheDay()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(

View file

@ -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);
}
}
}

View file

@ -10,36 +10,41 @@ import javax.inject.Named;
import javax.inject.Singleton;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.actions.PageEditClient;
import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.utils.ViewUtil;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
import timber.log.Timber;
/**
* This class is meant to handle the Wikidata edits made through the app
* It will talk with MediaWikiApi to make necessary API calls, log the edits and fire listeners
* It will talk with MediaWiki Apis to make the necessary calls, log the edits and fire listeners
* on successful edits
*/
@Singleton
public class WikidataEditService {
private final static String COMMONS_APP_TAG = "wikimedia-commons-app";
private final static String COMMONS_APP_EDIT_REASON = "Add tag for edits made using Android Commons app";
private final Context context;
private final MediaWikiApi mediaWikiApi;
private final WikidataEditListener wikidataEditListener;
private final JsonKvStore directKvStore;
private final WikidataClient wikidataClient;
private final PageEditClient wikiDataPageEditClient;
@Inject
public WikidataEditService(Context context,
MediaWikiApi mediaWikiApi,
WikidataEditListener wikidataEditListener,
@Named("default_preferences") JsonKvStore directKvStore) {
@Named("default_preferences") JsonKvStore directKvStore,
WikidataClient wikidataClient,
@Named("wikidata-page-edit") PageEditClient wikiDataPageEditClient) {
this.context = context;
this.mediaWikiApi = mediaWikiApi;
this.wikidataEditListener = wikidataEditListener;
this.directKvStore = directKvStore;
this.wikidataClient = wikidataClient;
this.wikiDataPageEditClient = wikiDataPageEditClient;
}
/**
@ -77,13 +82,20 @@ public class WikidataEditService {
private void editWikidataProperty(String wikidataEntityId, String fileName) {
Timber.d("Upload successful with wiki data entity id as %s", wikidataEntityId);
Timber.d("Attempting to edit Wikidata property %s", wikidataEntityId);
Observable.fromCallable(() -> {
String propertyValue = getFileName(fileName);
return mediaWikiApi.wikidataCreateClaim(wikidataEntityId, "P18", "value", propertyValue);
})
String propertyValue = getFileName(fileName);
Timber.d(propertyValue);
wikidataClient.createClaim(wikidataEntityId, "P18", "value", propertyValue)
.flatMap(revisionId -> {
if (revisionId != -1) {
return wikiDataPageEditClient.addEditTag(revisionId, COMMONS_APP_TAG, COMMONS_APP_EDIT_REASON);
}
throw new RuntimeException("Unable to edit wikidata item");
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(revisionId -> handleClaimResult(wikidataEntityId, revisionId), throwable -> {
.subscribe(revisionId -> handleClaimResult(wikidataEntityId, String.valueOf(revisionId)), throwable -> {
Timber.e(throwable, "Error occurred while making claim");
ViewUtil.showLongToast(context, context.getString(R.string.wikidata_edit_failure));
});
@ -95,31 +107,12 @@ public class WikidataEditService {
wikidataEditListener.onSuccessfulWikidataEdit();
}
showSuccessToast();
logEdit(revisionId);
} else {
Timber.d("Unable to make wiki data edit for entity %s", wikidataEntityId);
ViewUtil.showLongToast(context, context.getString(R.string.wikidata_edit_failure));
}
}
/**
* Log the Wikidata edit by adding Wikimedia Commons App tag to the edit
* @param revisionId
*/
@SuppressLint("CheckResult")
private void logEdit(String revisionId) {
Observable.fromCallable(() -> mediaWikiApi.addWikidataEditTag(revisionId))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(result -> {
if (result) {
Timber.d("Wikidata edit was tagged successfully");
} else {
Timber.d("Wikidata edit couldn't be tagged");
}
}, throwable -> Timber.e(throwable, "Error occurred while adding tag to the edit"));
}
/**
* Show a success toast when the edit is made successfully
*/

View file

@ -150,7 +150,7 @@
android:layout_height="wrap_content"
android:hint="@string/_2fa_code"
android:imeOptions="flagNoExtractUi"
android:inputType="textNoSuggestions"
android:inputType="number"
android:visibility="gone"
tools:visibility="visible" />

View file

@ -151,7 +151,7 @@
android:layout_height="wrap_content"
android:hint="@string/_2fa_code"
android:imeOptions="flagNoExtractUi"
android:inputType="textNoSuggestions"
android:inputType="number"
android:visibility="gone"
tools:visibility="visible" />

View file

@ -156,7 +156,7 @@
android:layout_height="wrap_content"
android:hint="@string/_2fa_code"
android:imeOptions="flagNoExtractUi"
android:inputType="textNoSuggestions"
android:inputType="number"
android:visibility="gone"
tools:visibility="visible" />

View file

@ -1,11 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Authors:
* Ibrahim
* ToJack
-->
<resources>
<string name="crash_dialog_title">Хаиогии Викианбор</string>
<string name="crash_dialog_text">Упс. Чизе хато пеш рафт!</string>
<string name="crash_dialog_comment_prompt">Ба мо бигуед, ки шумо чӣ кор кардед, ба мо дар имайл нависед. Мо ба шумо кӯмак мерасонем.</string>
<string name="crash_dialog_ok_toast">Ташаккур!</string>
</resources>

View file

@ -1,9 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Authors:
* Ibrahim
* ToJack
* Vashgird
-->
<resources>
<string name="title_activity_explore">Омӯзиш</string>
<string name="navigation_item_explore">Омӯзиш</string>
@ -12,9 +7,7 @@
<string name="preference_category_feedback">Пешниҳод</string>
<string name="preference_category_privacy">Ҳарими хусусӣ</string>
<string name="preference_category_location">Мавқеъ</string>
<string name="app_name">Викианбор</string>
<string name="menu_settings">Танзимот</string>
<string name="intent_share_upload_label">Ирсолнамоӣ ба Викианбор</string>
<string name="username">Номи корбар</string>
<string name="password">Гузарвожа</string>
<string name="login_credential">Вурудшавӣ ба ҳисоби Commons Beta</string>
@ -35,11 +28,10 @@
<string name="contribution_state_starting">Дар ҳоли боркунӣ</string>
<string name="menu_from_gallery">Аз Нигористон</string>
<string name="menu_from_camera">Гирифтани акс</string>
<string name="menu_nearby">Наздикӣ</string>
<string name="provider_contributions">Боргузориҳои ман</string>
<string name="menu_share">Бо ҳам дидан</string>
<string name="menu_open_in_browser">Дидан дар Мурургар</string>
<string name="share_title_hint" fuzzy="true">Унвон</string>
<string name="share_title_hint">Унвон</string>
<string name="share_description_hint">Тавзеҳот</string>
<string name="login_failed_generic">Вуруд номуваффақ шуд</string>
<string name="share_upload_button">Боркунӣ</string>
@ -97,26 +89,6 @@
<string name="media_detail_uploaded_date">Рӯзи боркунӣ</string>
<string name="media_detail_license">Иҷозатнома</string>
<string name="media_detail_coordinates">Координатҳо</string>
<string name="media_detail_coordinates_empty">Пешниҳод нашудааст</string>
<string name="commons_logo">Логотипи Викианбор</string>
<string name="commons_website">Сомонаи Викианбор</string>
<string name="commons_facebook">Викианбор дар Фейсбук</string>
<string name="no_image_found">Аксе пайдо нашуд</string>
<string name="no_subcategory_found">Ягон зергурӯҳе пайдо нашуд</string>
<string name="welcome_image_welcome_wikipedia">Ба Википедиа хуш омадед.</string>
<string name="cancel">Пӯшидан</string>
<string name="navigation_drawer_open">Кушодан</string>
<string name="navigation_drawer_close">Пӯшидан</string>
<string name="navigation_item_home">Хона</string>
<string name="navigation_item_upload">Боркунӣ</string>
<string name="navigation_item_nearby">Наздикӣ</string>
<string name="navigation_item_about">Дар бораи</string>
<string name="navigation_item_settings">Танзимот</string>
<string name="navigation_item_feedback">Пешниҳод</string>
<string name="navigation_item_logout">Баромад</string>
<string name="navigation_item_notification">Огоҳиҳо</string>
<string name="navigation_item_featured_images">Баргузида</string>
<string name="navigation_item_review">Тафтиш</string>
<string name="nearby_info_menu_wikidata_article">Унсури Викидода</string>
<string name="error_while_cache">Хатогӣ ҳангоми кэшкунии тасвир</string>
<string name="navigation_item_login">Вуруд</string>

View file

@ -553,7 +553,6 @@
<string name="delete_helper_show_deletion_title_success">Éxito</string>
<string name="delete_helper_show_deletion_message_if" fuzzy="true">Se nominó exitosamente %1$s para ser borrada.</string>
<string name="delete_helper_show_deletion_title_failed">Falló</string>
<string name="delete_helper_show_deletion_message_else">No se pudo solicitar el borrado.</string>
<string name="delete_helper_ask_spam_selfie">Un autorretrato</string>
<string name="delete_helper_ask_spam_blurry">Borrosa</string>
<string name="delete_helper_ask_spam_nonsense">Sinsentido</string>

Some files were not shown because too many files have changed in this diff Show more