mirror of
https://github.com/commons-app/apps-android-commons.git
synced 2025-10-26 20:33:53 +01:00
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 commit0090f24257, reversing changes made to9bccbfe443. * * 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 commit42539651de. * 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 commit3dc084415b. * 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 commitf3451745d3, reversing changes made toc5d4d5533d. * 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:
parent
2ed32162b7
commit
8ccad2277d
119 changed files with 2558 additions and 5016 deletions
|
|
@ -1,7 +1,6 @@
|
|||
plugins {
|
||||
id 'com.github.triplet.play' version '2.2.1' apply false
|
||||
}
|
||||
|
||||
apply from: '../gitutils.gradle'
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
|
|
@ -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\""
|
||||
|
|
|
|||
|
|
@ -152,7 +152,8 @@
|
|||
</service>
|
||||
<service
|
||||
android:name=".contributions.ContributionsSyncService"
|
||||
android:exported="true">
|
||||
android:exported="true"
|
||||
android:process=":sync">
|
||||
<intent-filter>
|
||||
<action android:name="android.content.SyncAdapter" />
|
||||
</intent-filter>
|
||||
|
|
@ -161,17 +162,6 @@
|
|||
android:name="android.content.SyncAdapter"
|
||||
android:resource="@xml/contributions_sync_adapter" />
|
||||
</service>
|
||||
<service
|
||||
android:name=".modifications.ModificationsSyncService"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.content.SyncAdapter" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.content.SyncAdapter"
|
||||
android:resource="@xml/modifications_sync_adapter" />
|
||||
</service>
|
||||
|
||||
<service
|
||||
android:name="org.acra.sender.SenderService"
|
||||
|
|
@ -193,12 +183,7 @@
|
|||
android:exported="false"
|
||||
android:label="@string/provider_contributions"
|
||||
android:syncable="true" />
|
||||
<provider
|
||||
android:name=".modifications.ModificationsContentProvider"
|
||||
android:authorities="${applicationId}.modifications.contentprovider"
|
||||
android:exported="false"
|
||||
android:label="@string/provider_modifications"
|
||||
android:syncable="true" />
|
||||
|
||||
<provider
|
||||
android:name=".category.CategoryContentProvider"
|
||||
android:authorities="${applicationId}.categories.contentprovider"
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ public class CommonsAppAdapter extends AppAdapter {
|
|||
|
||||
@Override
|
||||
public void updateAccount(@NonNull LoginResult result) {
|
||||
// TODO: sessionManager.updateAccount(result);
|
||||
sessionManager.updateAccount(result);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -69,7 +69,8 @@ public class CommonsAppAdapter extends AppAdapter {
|
|||
if (!preferences.contains(COOKIE_STORE_NAME)) {
|
||||
return null;
|
||||
}
|
||||
return GsonUnmarshaller.unmarshal(SharedPreferenceCookieManager.class, preferences.getString(COOKIE_STORE_NAME, null));
|
||||
return GsonUnmarshaller.unmarshal(SharedPreferenceCookieManager.class,
|
||||
preferences.getString(COOKIE_STORE_NAME, null));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@ import android.net.Uri;
|
|||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.wikipedia.dataclient.mwapi.MwQueryPage;
|
||||
import org.wikipedia.gallery.ExtMetadata;
|
||||
|
|
@ -20,14 +23,13 @@ import java.util.List;
|
|||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import fr.free.nrw.commons.location.LatLng;
|
||||
import fr.free.nrw.commons.utils.CommonsDateUtil;
|
||||
import fr.free.nrw.commons.utils.MediaDataExtractorUtil;
|
||||
|
||||
public class Media implements Parcelable {
|
||||
|
||||
public static final Media EMPTY = new Media("");
|
||||
public static Creator<Media> CREATOR = new Creator<Media>() {
|
||||
@Override
|
||||
public Media createFromParcel(Parcel parcel) {
|
||||
|
|
@ -156,9 +158,9 @@ public class Media implements Parcelable {
|
|||
page.title(),
|
||||
"",
|
||||
0,
|
||||
safeParseDate(metadata.dateTimeOriginal().value()),
|
||||
safeParseDate(metadata.dateTime().value()),
|
||||
StringUtil.fromHtml(metadata.artist().value()).toString()
|
||||
safeParseDate(metadata.dateTime()),
|
||||
safeParseDate(metadata.dateTime()),
|
||||
StringUtil.fromHtml(metadata.artist()).toString()
|
||||
);
|
||||
|
||||
if (!StringUtils.isBlank(imageInfo.getThumbUrl())) {
|
||||
|
|
@ -170,17 +172,17 @@ public class Media implements Parcelable {
|
|||
language = "default";
|
||||
}
|
||||
|
||||
media.setDescriptions(Collections.singletonMap(language, metadata.imageDescription().value()));
|
||||
media.setCategories(MediaDataExtractorUtil.extractCategoriesFromList(metadata.categories().value()));
|
||||
String latitude = metadata.gpsLatitude().value();
|
||||
String longitude = metadata.gpsLongitude().value();
|
||||
media.setDescriptions(Collections.singletonMap(language, metadata.imageDescription()));
|
||||
media.setCategories(MediaDataExtractorUtil.extractCategoriesFromList(metadata.getCategories()));
|
||||
String latitude = metadata.getGpsLatitude();
|
||||
String longitude = metadata.getGpsLongitude();
|
||||
|
||||
if (!StringUtils.isBlank(latitude) && !StringUtils.isBlank(longitude)) {
|
||||
LatLng latLng = new LatLng(Double.parseDouble(latitude), Double.parseDouble(longitude), 0);
|
||||
media.setCoordinates(latLng);
|
||||
}
|
||||
|
||||
media.setLicenseInformation(metadata.licenseShortName().value(), metadata.licenseUrl().value());
|
||||
media.setLicenseInformation(metadata.licenseShortName(), metadata.licenseUrl());
|
||||
return media;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,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");
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,58 @@
|
|||
package fr.free.nrw.commons.actions;
|
||||
|
||||
import org.wikipedia.csrf.CsrfTokenClient;
|
||||
import org.wikipedia.dataclient.Service;
|
||||
|
||||
import io.reactivex.Observable;
|
||||
import timber.log.Timber;
|
||||
|
||||
public class PageEditClient {
|
||||
|
||||
private final CsrfTokenClient csrfTokenClient;
|
||||
private final PageEditInterface pageEditInterface;
|
||||
private final Service service;
|
||||
|
||||
public PageEditClient(CsrfTokenClient csrfTokenClient,
|
||||
PageEditInterface pageEditInterface,
|
||||
Service service) {
|
||||
this.csrfTokenClient = csrfTokenClient;
|
||||
this.pageEditInterface = pageEditInterface;
|
||||
this.service = service;
|
||||
}
|
||||
|
||||
public Observable<Boolean> edit(String pageTitle, String text, String summary) {
|
||||
try {
|
||||
return pageEditInterface.postEdit(pageTitle, summary, text, csrfTokenClient.getTokenBlocking())
|
||||
.map(editResponse -> editResponse.edit().editSucceeded());
|
||||
} catch (Throwable throwable) {
|
||||
return Observable.just(false);
|
||||
}
|
||||
}
|
||||
|
||||
public Observable<Boolean> appendEdit(String pageTitle, String appendText, String summary) {
|
||||
try {
|
||||
return pageEditInterface.postAppendEdit(pageTitle, summary, appendText, csrfTokenClient.getTokenBlocking())
|
||||
.map(editResponse -> editResponse.edit().editSucceeded());
|
||||
} catch (Throwable throwable) {
|
||||
return Observable.just(false);
|
||||
}
|
||||
}
|
||||
|
||||
public Observable<Boolean> prependEdit(String pageTitle, String prependText, String summary) {
|
||||
try {
|
||||
return pageEditInterface.postPrependEdit(pageTitle, summary, prependText, csrfTokenClient.getTokenBlocking())
|
||||
.map(editResponse -> editResponse.edit().editSucceeded());
|
||||
} catch (Throwable throwable) {
|
||||
return Observable.just(false);
|
||||
}
|
||||
}
|
||||
|
||||
public Observable<Integer> addEditTag(long revisionId, String tagName, String reason) {
|
||||
try {
|
||||
return service.addEditTag(String.valueOf(revisionId), tagName, reason, csrfTokenClient.getTokenBlocking())
|
||||
.map(mwPostResponse -> mwPostResponse.getSuccessVal());
|
||||
} catch (Throwable throwable) {
|
||||
return Observable.just(-1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
package fr.free.nrw.commons.actions;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.wikipedia.edit.Edit;
|
||||
|
||||
import io.reactivex.Observable;
|
||||
import retrofit2.http.Field;
|
||||
import retrofit2.http.FormUrlEncoded;
|
||||
import retrofit2.http.Headers;
|
||||
import retrofit2.http.POST;
|
||||
|
||||
import static org.wikipedia.dataclient.Service.MW_API_PREFIX;
|
||||
|
||||
public interface PageEditInterface {
|
||||
|
||||
@FormUrlEncoded
|
||||
@Headers("Cache-Control: no-cache")
|
||||
@POST(MW_API_PREFIX + "action=edit")
|
||||
@NonNull
|
||||
Observable<Edit> postEdit(@NonNull @Field("title") String title,
|
||||
@NonNull @Field("summary") String summary,
|
||||
@NonNull @Field("text") String text,
|
||||
@NonNull @Field("token") String token);
|
||||
|
||||
@FormUrlEncoded
|
||||
@Headers("Cache-Control: no-cache")
|
||||
@POST(MW_API_PREFIX + "action=edit")
|
||||
@NonNull Observable<Edit> postAppendEdit(@NonNull @Field("title") String title,
|
||||
@NonNull @Field("summary") String summary,
|
||||
@NonNull @Field("appendtext") String text,
|
||||
@NonNull @Field("token") String token);
|
||||
|
||||
@FormUrlEncoded
|
||||
@Headers("Cache-Control: no-cache")
|
||||
@POST(MW_API_PREFIX + "action=edit")
|
||||
@NonNull Observable<Edit> postPrependEdit(@NonNull @Field("title") String title,
|
||||
@NonNull @Field("summary") String summary,
|
||||
@NonNull @Field("prependtext") String text,
|
||||
@NonNull @Field("token") String token);
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
package fr.free.nrw.commons.actions;
|
||||
|
||||
import org.wikipedia.csrf.CsrfTokenClient;
|
||||
import org.wikipedia.dataclient.Service;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import fr.free.nrw.commons.CommonsApplication;
|
||||
import io.reactivex.Observable;
|
||||
|
||||
@Singleton
|
||||
public class ThanksClient {
|
||||
|
||||
private final CsrfTokenClient csrfTokenClient;
|
||||
private final Service service;
|
||||
|
||||
@Inject
|
||||
public ThanksClient(@Named("commons-csrf") CsrfTokenClient csrfTokenClient,
|
||||
@Named("commons-service") Service service) {
|
||||
this.csrfTokenClient = csrfTokenClient;
|
||||
this.service = service;
|
||||
}
|
||||
|
||||
public Observable<Boolean> thank(long revisionId) {
|
||||
try {
|
||||
return service.thank(String.valueOf(revisionId), null,
|
||||
csrfTokenClient.getTokenBlocking(),
|
||||
CommonsApplication.getInstance().getUserAgent())
|
||||
.map(mwQueryResponse -> mwQueryResponse.getSuccessVal() == 1);
|
||||
} catch (Throwable throwable) {
|
||||
return Observable.just(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,8 +3,8 @@ package fr.free.nrw.commons.auth;
|
|||
import android.accounts.Account;
|
||||
import android.accounts.AccountManager;
|
||||
import android.content.Context;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import fr.free.nrw.commons.BuildConfig;
|
||||
import timber.log.Timber;
|
||||
|
||||
|
|
@ -12,10 +12,8 @@ public class AccountUtil {
|
|||
|
||||
public static final String AUTH_COOKIE = "authCookie";
|
||||
public static final String AUTH_TOKEN_TYPE = "CommonsAndroid";
|
||||
private final Context context;
|
||||
|
||||
public AccountUtil(Context context) {
|
||||
this.context = context;
|
||||
public AccountUtil() {
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -49,5 +47,4 @@ public class AccountUtil {
|
|||
private static AccountManager accountManager(Context context) {
|
||||
return AccountManager.get(context);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
36
app/src/main/java/fr/free/nrw/commons/auth/LogoutClient.java
Normal file
36
app/src/main/java/fr/free/nrw/commons/auth/LogoutClient.java
Normal 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())));
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,125 @@
|
|||
package fr.free.nrw.commons.category;
|
||||
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.wikipedia.dataclient.mwapi.MwQueryPage;
|
||||
import org.wikipedia.dataclient.mwapi.MwQueryResponse;
|
||||
import org.wikipedia.dataclient.mwapi.MwQueryResult;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import io.reactivex.Observable;
|
||||
import timber.log.Timber;
|
||||
|
||||
/**
|
||||
* Category Client to handle custom calls to Commons MediaWiki APIs
|
||||
*/
|
||||
@Singleton
|
||||
public class CategoryClient {
|
||||
|
||||
private final CategoryInterface CategoryInterface;
|
||||
|
||||
@Inject
|
||||
public CategoryClient(CategoryInterface CategoryInterface) {
|
||||
this.CategoryInterface = CategoryInterface;
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches for categories containing the specified string.
|
||||
*
|
||||
* @param filter The string to be searched
|
||||
* @param itemLimit How many results are returned
|
||||
* @param offset Starts returning items from the nth result. If offset is 9, the response starts with the 9th item of the search result
|
||||
* @return
|
||||
*/
|
||||
public Observable<String> searchCategories(String filter, int itemLimit, int offset) {
|
||||
return responseToCategoryName(CategoryInterface.searchCategories(filter, itemLimit, offset));
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches for categories containing the specified string.
|
||||
*
|
||||
* @param filter The string to be searched
|
||||
* @param itemLimit How many results are returned
|
||||
* @return
|
||||
*/
|
||||
public Observable<String> searchCategories(String filter, int itemLimit) {
|
||||
return searchCategories(filter, itemLimit, 0);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches for categories starting with the specified string.
|
||||
*
|
||||
* @param prefix The prefix to be searched
|
||||
* @param itemLimit How many results are returned
|
||||
* @param offset Starts returning items from the nth result. If offset is 9, the response starts with the 9th item of the search result
|
||||
* @return
|
||||
*/
|
||||
public Observable<String> searchCategoriesForPrefix(String prefix, int itemLimit, int offset) {
|
||||
return responseToCategoryName(CategoryInterface.searchCategoriesForPrefix(prefix, itemLimit, offset));
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches for categories starting with the specified string.
|
||||
*
|
||||
* @param prefix The prefix to be searched
|
||||
* @param itemLimit How many results are returned
|
||||
* @return
|
||||
*/
|
||||
public Observable<String> searchCategoriesForPrefix(String prefix, int itemLimit) {
|
||||
return searchCategoriesForPrefix(prefix, itemLimit, 0);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* The method takes categoryName as input and returns a List of Subcategories
|
||||
* It uses the generator query API to get the subcategories in a category, 500 at a time.
|
||||
*
|
||||
* @param categoryName Category name as defined on commons
|
||||
* @return Observable emitting the categories returned. If our search yielded "Category:Test", "Test" is emitted.
|
||||
*/
|
||||
public Observable<String> getSubCategoryList(String categoryName) {
|
||||
return responseToCategoryName(CategoryInterface.getSubCategoryList(categoryName));
|
||||
}
|
||||
|
||||
/**
|
||||
* The method takes categoryName as input and returns a List of parent categories
|
||||
* It uses the generator query API to get the parent categories of a category, 500 at a time.
|
||||
*
|
||||
* @param categoryName Category name as defined on commons
|
||||
* @return
|
||||
*/
|
||||
@NonNull
|
||||
public Observable<String> getParentCategoryList(String categoryName) {
|
||||
return responseToCategoryName(CategoryInterface.getParentCategoryList(categoryName));
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal function to reduce code reuse. Extracts the categories returned from MwQueryResponse.
|
||||
*
|
||||
* @param responseObservable The query response observable
|
||||
* @return Observable emitting the categories returned. If our search yielded "Category:Test", "Test" is emitted.
|
||||
*/
|
||||
private Observable<String> responseToCategoryName(Observable<MwQueryResponse> responseObservable) {
|
||||
return responseObservable
|
||||
.flatMap(mwQueryResponse -> {
|
||||
MwQueryResult query;
|
||||
List<MwQueryPage> pages;
|
||||
if ((query = mwQueryResponse.query()) == null ||
|
||||
(pages = query.pages()) == null) {
|
||||
Timber.d("No categories returned.");
|
||||
return Observable.empty();
|
||||
} else
|
||||
return Observable.fromIterable(pages);
|
||||
})
|
||||
.map(MwQueryPage::title)
|
||||
.doOnEach(s -> Timber.d("Category returned: %s", s))
|
||||
.map(cat -> cat.replace("Category:", ""));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
package fr.free.nrw.commons.category;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import fr.free.nrw.commons.Media;
|
||||
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
|
||||
import io.reactivex.Single;
|
||||
|
||||
@Singleton
|
||||
public class CategoryImageController {
|
||||
|
||||
private OkHttpJsonApiClient okHttpJsonApiClient;
|
||||
|
||||
@Inject
|
||||
public CategoryImageController(OkHttpJsonApiClient okHttpJsonApiClient) {
|
||||
this.okHttpJsonApiClient = okHttpJsonApiClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a category name as input and calls the API to get a list of images for that category
|
||||
* @param categoryName
|
||||
* @return
|
||||
*/
|
||||
public Single<List<Media>> getCategoryImages(String categoryName) {
|
||||
return okHttpJsonApiClient.getMediaList("category", categoryName);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
package fr.free.nrw.commons.category;
|
||||
|
||||
import org.w3c.dom.Element;
|
||||
import org.w3c.dom.Node;
|
||||
import org.w3c.dom.NodeList;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class CategoryImageUtils {
|
||||
|
||||
/**
|
||||
* The method iterates over the child nodes to return a list of Subcategory name
|
||||
* sorted alphabetically
|
||||
* @param childNodes
|
||||
* @return
|
||||
*/
|
||||
public static List<String> getSubCategoryList(NodeList childNodes) {
|
||||
List<String> subCategories = new ArrayList<>();
|
||||
for (int i = 0; i < childNodes.getLength(); i++) {
|
||||
Node node = childNodes.item(i);
|
||||
subCategories.add(getFileName(node));
|
||||
}
|
||||
Collections.sort(subCategories);
|
||||
return subCategories;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the filename of the uploaded image
|
||||
* @param document
|
||||
* @return
|
||||
*/
|
||||
private static String getFileName(Node document) {
|
||||
Element element = (Element) document;
|
||||
return element.getAttribute("title");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -2,20 +2,19 @@ package fr.free.nrw.commons.category;
|
|||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.database.DataSetObserver;
|
||||
import android.os.Bundle;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentTransaction;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.AdapterView;
|
||||
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentTransaction;
|
||||
|
||||
import butterknife.ButterKnife;
|
||||
import fr.free.nrw.commons.Media;
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.auth.AuthenticatedActivity;
|
||||
import fr.free.nrw.commons.explore.SearchActivity;
|
||||
import fr.free.nrw.commons.media.MediaDetailPagerFragment;
|
||||
import fr.free.nrw.commons.theme.NavigationBaseActivity;
|
||||
|
|
@ -28,7 +27,7 @@ import fr.free.nrw.commons.theme.NavigationBaseActivity;
|
|||
*/
|
||||
|
||||
public class CategoryImagesActivity
|
||||
extends AuthenticatedActivity
|
||||
extends NavigationBaseActivity
|
||||
implements FragmentManager.OnBackStackChangedListener,
|
||||
MediaDetailPagerFragment.MediaDetailProvider,
|
||||
AdapterView.OnItemClickListener{
|
||||
|
|
@ -38,16 +37,6 @@ public class CategoryImagesActivity
|
|||
private CategoryImagesListFragment categoryImagesListFragment;
|
||||
private MediaDetailPagerFragment mediaDetails;
|
||||
|
||||
@Override
|
||||
protected void onAuthCookieAcquired(String authCookie) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onAuthFailure() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called on backPressed of anyFragment in the activity.
|
||||
* We are changing the icon here from back to hamburger icon.
|
||||
|
|
@ -69,7 +58,6 @@ public class CategoryImagesActivity
|
|||
supportFragmentManager = getSupportFragmentManager();
|
||||
setCategoryImagesFragment();
|
||||
supportFragmentManager.addOnBackStackChangedListener(this);
|
||||
requestAuthToken();
|
||||
initDrawer();
|
||||
setPageTitle();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import fr.free.nrw.commons.Media;
|
|||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.explore.categories.ExploreActivity;
|
||||
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
||||
import fr.free.nrw.commons.media.MediaClient;
|
||||
import fr.free.nrw.commons.utils.NetworkUtils;
|
||||
import fr.free.nrw.commons.utils.ViewUtil;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
|
|
@ -56,7 +57,7 @@ public class CategoryImagesListFragment extends DaggerFragment {
|
|||
private boolean isLoading = true;
|
||||
private String categoryName = null;
|
||||
|
||||
@Inject CategoryImageController controller;
|
||||
@Inject MediaClient mediaClient;
|
||||
@Inject
|
||||
@Named("default_preferences")
|
||||
JsonKvStore categoryKvStore;
|
||||
|
|
@ -116,7 +117,7 @@ public class CategoryImagesListFragment extends DaggerFragment {
|
|||
|
||||
isLoading = true;
|
||||
progressBar.setVisibility(VISIBLE);
|
||||
compositeDisposable.add(controller.getCategoryImages(categoryName)
|
||||
compositeDisposable.add(mediaClient.getMediaListFromCategory(categoryName)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
|
||||
|
|
@ -222,7 +223,7 @@ public class CategoryImagesListFragment extends DaggerFragment {
|
|||
}
|
||||
|
||||
progressBar.setVisibility(VISIBLE);
|
||||
compositeDisposable.add(controller.getCategoryImages(categoryName)
|
||||
compositeDisposable.add(mediaClient.getMediaListFromCategory(categoryName)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,47 @@
|
|||
package fr.free.nrw.commons.category;
|
||||
|
||||
import org.wikipedia.dataclient.mwapi.MwQueryResponse;
|
||||
|
||||
import io.reactivex.Observable;
|
||||
import retrofit2.http.GET;
|
||||
import retrofit2.http.Query;
|
||||
|
||||
/**
|
||||
* Interface for interacting with Commons category related APIs
|
||||
*/
|
||||
public interface CategoryInterface {
|
||||
|
||||
/**
|
||||
* Searches for categories with the specified name.
|
||||
*
|
||||
* @param filter The string to be searched
|
||||
* @param itemLimit How many results are returned
|
||||
* @return
|
||||
*/
|
||||
@GET("w/api.php?action=query&format=json&formatversion=2"
|
||||
+ "&generator=search&gsrnamespace=14")
|
||||
Observable<MwQueryResponse> searchCategories(@Query("gsrsearch") String filter,
|
||||
@Query("gsrlimit") int itemLimit, @Query("gsroffset") int offset);
|
||||
|
||||
/**
|
||||
* Searches for categories starting with the specified prefix.
|
||||
*
|
||||
* @param prefix The string to be searched
|
||||
* @param itemLimit How many results are returned
|
||||
* @return
|
||||
*/
|
||||
@GET("w/api.php?action=query&format=json&formatversion=2"
|
||||
+ "&generator=allcategories")
|
||||
Observable<MwQueryResponse> searchCategoriesForPrefix(@Query("gacprefix") String prefix,
|
||||
@Query("gaclimit") int itemLimit, @Query("gacoffset") int offset);
|
||||
|
||||
@GET("w/api.php?action=query&format=json&formatversion=2"
|
||||
+ "&generator=categorymembers&gcmtype=subcat"
|
||||
+ "&prop=info&gcmlimit=500")
|
||||
Observable<MwQueryResponse> getSubCategoryList(@Query("gcmtitle") String categoryName);
|
||||
|
||||
@GET("w/api.php?action=query&format=json&formatversion=2"
|
||||
+ "&generator=categories&prop=info&gcllimit=500")
|
||||
Observable<MwQueryResponse> getParentCategoryList(@Query("titles") String categoryName);
|
||||
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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!");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesDao;
|
|||
import fr.free.nrw.commons.category.CategoryDao;
|
||||
import fr.free.nrw.commons.contributions.ContributionDao;
|
||||
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao;
|
||||
import fr.free.nrw.commons.modifications.ModifierSequenceDao;
|
||||
|
||||
public class DBOpenHelper extends SQLiteOpenHelper {
|
||||
|
||||
|
|
@ -27,7 +26,6 @@ public class DBOpenHelper extends SQLiteOpenHelper {
|
|||
@Override
|
||||
public void onCreate(SQLiteDatabase sqLiteDatabase) {
|
||||
ContributionDao.Table.onCreate(sqLiteDatabase);
|
||||
ModifierSequenceDao.Table.onCreate(sqLiteDatabase);
|
||||
CategoryDao.Table.onCreate(sqLiteDatabase);
|
||||
BookmarkPicturesDao.Table.onCreate(sqLiteDatabase);
|
||||
BookmarkLocationsDao.Table.onCreate(sqLiteDatabase);
|
||||
|
|
@ -37,7 +35,6 @@ public class DBOpenHelper extends SQLiteOpenHelper {
|
|||
@Override
|
||||
public void onUpgrade(SQLiteDatabase sqLiteDatabase, int from, int to) {
|
||||
ContributionDao.Table.onUpdate(sqLiteDatabase, from, to);
|
||||
ModifierSequenceDao.Table.onUpdate(sqLiteDatabase, from, to);
|
||||
CategoryDao.Table.onUpdate(sqLiteDatabase, from, to);
|
||||
BookmarkPicturesDao.Table.onUpdate(sqLiteDatabase, from, to);
|
||||
BookmarkLocationsDao.Table.onUpdate(sqLiteDatabase, from, to);
|
||||
|
|
|
|||
|
|
@ -11,19 +11,22 @@ import java.text.SimpleDateFormat;
|
|||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.Callable;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import fr.free.nrw.commons.BuildConfig;
|
||||
import fr.free.nrw.commons.Media;
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.auth.SessionManager;
|
||||
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
||||
import fr.free.nrw.commons.actions.PageEditClient;
|
||||
import fr.free.nrw.commons.notification.NotificationHelper;
|
||||
import fr.free.nrw.commons.review.ReviewController;
|
||||
import fr.free.nrw.commons.utils.ViewUtilWrapper;
|
||||
import io.reactivex.Observable;
|
||||
import io.reactivex.Single;
|
||||
import io.reactivex.SingleSource;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import timber.log.Timber;
|
||||
|
|
@ -35,20 +38,20 @@ import static fr.free.nrw.commons.notification.NotificationHelper.NOTIFICATION_D
|
|||
*/
|
||||
@Singleton
|
||||
public class DeleteHelper {
|
||||
private final MediaWikiApi mwApi;
|
||||
private final SessionManager sessionManager;
|
||||
private final NotificationHelper notificationHelper;
|
||||
private final PageEditClient pageEditClient;
|
||||
private final ViewUtilWrapper viewUtil;
|
||||
private final String username;
|
||||
|
||||
@Inject
|
||||
public DeleteHelper(MediaWikiApi mwApi,
|
||||
SessionManager sessionManager,
|
||||
NotificationHelper notificationHelper,
|
||||
ViewUtilWrapper viewUtil) {
|
||||
this.mwApi = mwApi;
|
||||
this.sessionManager = sessionManager;
|
||||
public DeleteHelper(NotificationHelper notificationHelper,
|
||||
@Named("commons-page-edit") PageEditClient pageEditClient,
|
||||
ViewUtilWrapper viewUtil,
|
||||
@Named("username") String username) {
|
||||
this.notificationHelper = notificationHelper;
|
||||
this.pageEditClient = pageEditClient;
|
||||
this.viewUtil = viewUtil;
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -59,10 +62,11 @@ public class DeleteHelper {
|
|||
* @return
|
||||
*/
|
||||
public Single<Boolean> makeDeletion(Context context, Media media, String reason) {
|
||||
viewUtil.showShortToast(context, context.getString((R.string.delete_helper_make_deletion_toast), media.getDisplayTitle()));
|
||||
return Single.fromCallable(() -> delete(media, reason))
|
||||
.flatMap(result -> Single.fromCallable(() ->
|
||||
showDeletionNotification(context, media, result)));
|
||||
viewUtil.showShortToast(context, "Trying to nominate " + media.getDisplayTitle() + " for deletion");
|
||||
|
||||
return delete(media, reason)
|
||||
.flatMapSingle(result -> Single.just(showDeletionNotification(context, media, result)))
|
||||
.firstOrError();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -71,14 +75,9 @@ public class DeleteHelper {
|
|||
* @param reason
|
||||
* @return
|
||||
*/
|
||||
private boolean delete(Media media, String reason) {
|
||||
String editToken;
|
||||
String authCookie;
|
||||
private Observable<Boolean> delete(Media media, String reason) {
|
||||
Timber.d("thread is delete %s", Thread.currentThread().getName());
|
||||
String summary = "Nominating " + media.getFilename() + " for deletion.";
|
||||
|
||||
authCookie = sessionManager.getAuthCookie();
|
||||
mwApi.setAuthCookie(authCookie);
|
||||
|
||||
Calendar calendar = Calendar.getInstance();
|
||||
String fileDeleteString = "{{delete|reason=" + reason +
|
||||
"|subpage=" + media.getFilename() +
|
||||
|
|
@ -99,26 +98,23 @@ public class DeleteHelper {
|
|||
String userPageString = "\n{{subst:idw|" + media.getFilename() +
|
||||
"}} ~~~~";
|
||||
|
||||
try {
|
||||
editToken = mwApi.getEditToken();
|
||||
|
||||
if(editToken == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
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 -> {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
package fr.free.nrw.commons.di;
|
||||
|
||||
import fr.free.nrw.commons.contributions.ContributionsModule;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import dagger.Component;
|
||||
|
|
@ -10,8 +9,8 @@ import dagger.android.support.AndroidSupportInjectionModule;
|
|||
import fr.free.nrw.commons.CommonsApplication;
|
||||
import fr.free.nrw.commons.auth.LoginActivity;
|
||||
import fr.free.nrw.commons.contributions.ContributionViewHolder;
|
||||
import fr.free.nrw.commons.contributions.ContributionsModule;
|
||||
import fr.free.nrw.commons.contributions.ContributionsSyncAdapter;
|
||||
import fr.free.nrw.commons.modifications.ModificationsSyncAdapter;
|
||||
import fr.free.nrw.commons.nearby.PlaceRenderer;
|
||||
import fr.free.nrw.commons.review.ReviewController;
|
||||
import fr.free.nrw.commons.settings.SettingsFragment;
|
||||
|
|
@ -36,8 +35,6 @@ public interface CommonsApplicationComponent extends AndroidInjector<Application
|
|||
|
||||
void inject(ContributionsSyncAdapter syncAdapter);
|
||||
|
||||
void inject(ModificationsSyncAdapter syncAdapter);
|
||||
|
||||
void inject(LoginActivity activity);
|
||||
|
||||
void inject(SettingsFragment fragment);
|
||||
|
|
|
|||
|
|
@ -5,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(), "");
|
||||
}
|
||||
}
|
||||
|
|
@ -7,7 +7,6 @@ import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesContentProvider;
|
|||
import fr.free.nrw.commons.category.CategoryContentProvider;
|
||||
import fr.free.nrw.commons.contributions.ContributionsContentProvider;
|
||||
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesContentProvider;
|
||||
import fr.free.nrw.commons.modifications.ModificationsContentProvider;
|
||||
|
||||
@Module
|
||||
@SuppressWarnings({"WeakerAccess", "unused"})
|
||||
|
|
@ -16,9 +15,6 @@ public abstract class ContentProviderBuilderModule {
|
|||
@ContributesAndroidInjector
|
||||
abstract ContributionsContentProvider bindContributionsContentProvider();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract ModificationsContentProvider bindModificationsContentProvider();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract CategoryContentProvider bindCategoryContentProvider();
|
||||
|
||||
|
|
|
|||
|
|
@ -2,11 +2,16 @@ package fr.free.nrw.commons.di;
|
|||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
|
||||
import org.wikipedia.csrf.CsrfTokenClient;
|
||||
import org.wikipedia.dataclient.Service;
|
||||
import org.wikipedia.dataclient.ServiceFactory;
|
||||
import org.wikipedia.dataclient.WikiSite;
|
||||
import org.wikipedia.json.GsonUtil;
|
||||
import org.wikipedia.login.LoginClient;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
|
@ -14,15 +19,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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
173
app/src/main/java/fr/free/nrw/commons/media/MediaClient.java
Normal file
173
app/src/main/java/fr/free/nrw/commons/media/MediaClient.java
Normal 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("");
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
package fr.free.nrw.commons.modifications;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
public class CategoryModifier extends PageModifier {
|
||||
|
||||
public static String PARAM_CATEGORIES = "categories";
|
||||
|
||||
public static String MODIFIER_NAME = "CategoriesModifier";
|
||||
|
||||
public CategoryModifier(String... categories) {
|
||||
super(MODIFIER_NAME);
|
||||
JSONArray categoriesArray = new JSONArray();
|
||||
for (String category: categories) {
|
||||
categoriesArray.put(category);
|
||||
}
|
||||
try {
|
||||
params.putOpt(PARAM_CATEGORIES, categoriesArray);
|
||||
} catch (JSONException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public CategoryModifier(JSONObject data) {
|
||||
super(MODIFIER_NAME);
|
||||
this.params = data;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String doModification(String pageName, String pageContents) {
|
||||
JSONArray categories;
|
||||
categories = params.optJSONArray(PARAM_CATEGORIES);
|
||||
|
||||
StringBuilder categoriesString = new StringBuilder();
|
||||
for (int i = 0; i < categories.length(); i++) {
|
||||
String category = categories.optString(i);
|
||||
categoriesString.append("\n[[Category:").append(category).append("]]");
|
||||
}
|
||||
return pageContents + categoriesString.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getEditSummary() {
|
||||
return "Added " + params.optJSONArray(PARAM_CATEGORIES).length() + " categories.";
|
||||
}
|
||||
}
|
||||
|
|
@ -1,161 +0,0 @@
|
|||
package fr.free.nrw.commons.modifications;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.UriMatcher;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteQueryBuilder;
|
||||
import android.net.Uri;
|
||||
import androidx.annotation.NonNull;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import fr.free.nrw.commons.BuildConfig;
|
||||
import fr.free.nrw.commons.data.DBOpenHelper;
|
||||
import fr.free.nrw.commons.di.CommonsDaggerContentProvider;
|
||||
import timber.log.Timber;
|
||||
|
||||
import static fr.free.nrw.commons.modifications.ModifierSequenceDao.Table.TABLE_NAME;
|
||||
|
||||
public class ModificationsContentProvider extends CommonsDaggerContentProvider {
|
||||
|
||||
private static final int MODIFICATIONS = 1;
|
||||
private static final int MODIFICATIONS_ID = 2;
|
||||
|
||||
public static final String BASE_PATH = "modifications";
|
||||
|
||||
public static final Uri BASE_URI = Uri.parse("content://" + BuildConfig.MODIFICATION_AUTHORITY + "/" + BASE_PATH);
|
||||
|
||||
private static final UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
|
||||
static {
|
||||
uriMatcher.addURI(BuildConfig.MODIFICATION_AUTHORITY, BASE_PATH, MODIFICATIONS);
|
||||
uriMatcher.addURI(BuildConfig.MODIFICATION_AUTHORITY, BASE_PATH + "/#", MODIFICATIONS_ID);
|
||||
}
|
||||
|
||||
public static Uri uriForId(int id) {
|
||||
return Uri.parse(BASE_URI.toString() + "/" + id);
|
||||
}
|
||||
|
||||
@Inject DBOpenHelper dbOpenHelper;
|
||||
|
||||
@Override
|
||||
public Cursor query(@NonNull Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
|
||||
SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
|
||||
queryBuilder.setTables(TABLE_NAME);
|
||||
|
||||
int uriType = uriMatcher.match(uri);
|
||||
|
||||
switch (uriType) {
|
||||
case MODIFICATIONS:
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown URI" + uri);
|
||||
}
|
||||
|
||||
SQLiteDatabase db = dbOpenHelper.getReadableDatabase();
|
||||
|
||||
Cursor cursor = queryBuilder.query(db, projection, selection, selectionArgs, null, null, sortOrder);
|
||||
cursor.setNotificationUri(getContext().getContentResolver(), uri);
|
||||
|
||||
return cursor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getType(@NonNull Uri uri) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri insert(@NonNull Uri uri, ContentValues contentValues) {
|
||||
int uriType = uriMatcher.match(uri);
|
||||
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
|
||||
long id;
|
||||
switch (uriType) {
|
||||
case MODIFICATIONS:
|
||||
id = sqlDB.insert(TABLE_NAME, null, contentValues);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown URI: " + uri);
|
||||
}
|
||||
getContext().getContentResolver().notifyChange(uri, null);
|
||||
return Uri.parse(BASE_URI + "/" + id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int delete(@NonNull Uri uri, String s, String[] strings) {
|
||||
int uriType = uriMatcher.match(uri);
|
||||
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
|
||||
switch (uriType) {
|
||||
case MODIFICATIONS_ID:
|
||||
String id = uri.getLastPathSegment();
|
||||
sqlDB.delete(TABLE_NAME,
|
||||
"_id = ?",
|
||||
new String[] { id }
|
||||
);
|
||||
return 1;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown URI: " + uri);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int bulkInsert(@NonNull Uri uri, @NonNull ContentValues[] values) {
|
||||
Timber.d("Hello, bulk insert! (ModificationsContentProvider)");
|
||||
int uriType = uriMatcher.match(uri);
|
||||
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
|
||||
sqlDB.beginTransaction();
|
||||
switch (uriType) {
|
||||
case MODIFICATIONS:
|
||||
for (ContentValues value: values) {
|
||||
Timber.d("Inserting! %s", value);
|
||||
sqlDB.insert(TABLE_NAME, null, value);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown URI: " + uri);
|
||||
}
|
||||
sqlDB.setTransactionSuccessful();
|
||||
sqlDB.endTransaction();
|
||||
getContext().getContentResolver().notifyChange(uri, null);
|
||||
return values.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int update(@NonNull Uri uri, ContentValues contentValues, String selection, String[] selectionArgs) {
|
||||
/*
|
||||
SQL Injection warnings: First, note that we're not exposing this to the outside world (exported="false")
|
||||
Even then, we should make sure to sanitize all user input appropriately. Input that passes through ContentValues
|
||||
should be fine. So only issues are those that pass in via concating.
|
||||
|
||||
In here, the only concat created argument is for id. It is cast to an int, and will error out otherwise.
|
||||
*/
|
||||
int uriType = uriMatcher.match(uri);
|
||||
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
|
||||
int rowsUpdated;
|
||||
switch (uriType) {
|
||||
case MODIFICATIONS:
|
||||
rowsUpdated = sqlDB.update(TABLE_NAME,
|
||||
contentValues,
|
||||
selection,
|
||||
selectionArgs);
|
||||
break;
|
||||
case MODIFICATIONS_ID:
|
||||
int id = Integer.valueOf(uri.getLastPathSegment());
|
||||
|
||||
if (TextUtils.isEmpty(selection)) {
|
||||
rowsUpdated = sqlDB.update(TABLE_NAME,
|
||||
contentValues,
|
||||
ModifierSequenceDao.Table.COLUMN_ID + " = ?",
|
||||
new String[] { String.valueOf(id) } );
|
||||
} else {
|
||||
throw new IllegalArgumentException("Parameter `selection` should be empty when updating an ID");
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown URI: " + uri + " with type " + uriType);
|
||||
}
|
||||
getContext().getContentResolver().notifyChange(uri, null);
|
||||
return rowsUpdated;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,145 +0,0 @@
|
|||
package fr.free.nrw.commons.modifications;
|
||||
|
||||
import android.accounts.Account;
|
||||
import android.content.AbstractThreadedSyncAdapter;
|
||||
import android.content.ContentProviderClient;
|
||||
import android.content.Context;
|
||||
import android.content.SyncResult;
|
||||
import android.database.Cursor;
|
||||
import android.os.Bundle;
|
||||
import android.os.RemoteException;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import fr.free.nrw.commons.BuildConfig;
|
||||
import fr.free.nrw.commons.auth.SessionManager;
|
||||
import fr.free.nrw.commons.contributions.Contribution;
|
||||
import fr.free.nrw.commons.contributions.ContributionDao;
|
||||
import fr.free.nrw.commons.di.ApplicationlessInjection;
|
||||
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
||||
import timber.log.Timber;
|
||||
|
||||
public class ModificationsSyncAdapter extends AbstractThreadedSyncAdapter {
|
||||
|
||||
@Inject MediaWikiApi mwApi;
|
||||
@Inject ContributionDao contributionDao;
|
||||
@Inject ModifierSequenceDao modifierSequenceDao;
|
||||
@Inject
|
||||
SessionManager sessionManager;
|
||||
|
||||
public ModificationsSyncAdapter(Context context, boolean autoInitialize) {
|
||||
super(context, autoInitialize);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPerformSync(Account account, Bundle bundle, String s, ContentProviderClient contentProviderClient, SyncResult syncResult) {
|
||||
// This code is fraught with possibilities of race conditions, but lalalalala I can't hear you!
|
||||
ApplicationlessInjection
|
||||
.getInstance(getContext()
|
||||
.getApplicationContext())
|
||||
.getCommonsApplicationComponent()
|
||||
.inject(this);
|
||||
|
||||
Cursor allModifications;
|
||||
try {
|
||||
allModifications = contentProviderClient.query(ModificationsContentProvider.BASE_URI, null, null, null, null);
|
||||
} catch (RemoteException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
// Exit early if nothing to do
|
||||
if (allModifications == null || allModifications.getCount() == 0) {
|
||||
Timber.d("No modifications to perform");
|
||||
return;
|
||||
}
|
||||
|
||||
String authCookie = sessionManager.getAuthCookie();
|
||||
if (isNullOrWhiteSpace(authCookie)) {
|
||||
Timber.d("Could not authenticate :(");
|
||||
return;
|
||||
}
|
||||
|
||||
mwApi.setAuthCookie(authCookie);
|
||||
String editToken;
|
||||
|
||||
try {
|
||||
editToken = mwApi.getEditToken();
|
||||
} catch (IOException e) {
|
||||
Timber.d("Can not retreive edit token!");
|
||||
return;
|
||||
}
|
||||
|
||||
allModifications.moveToFirst();
|
||||
|
||||
Timber.d("Found %d modifications to execute", allModifications.getCount());
|
||||
|
||||
ContentProviderClient contributionsClient = null;
|
||||
try {
|
||||
contributionsClient = getContext().getContentResolver().acquireContentProviderClient(BuildConfig.CONTRIBUTION_AUTHORITY);
|
||||
|
||||
while (!allModifications.isAfterLast()) {
|
||||
ModifierSequence sequence = modifierSequenceDao.fromCursor(allModifications);
|
||||
Contribution contrib;
|
||||
Cursor contributionCursor;
|
||||
|
||||
if (contributionsClient == null) {
|
||||
Timber.e("ContributionsClient is null. This should not happen!");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
contributionCursor = contributionsClient.query(sequence.getMediaUri(), null, null, null, null);
|
||||
} catch (RemoteException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
if (contributionCursor != null) {
|
||||
contributionCursor.moveToFirst();
|
||||
}
|
||||
|
||||
contrib = contributionDao.fromCursor(contributionCursor);
|
||||
|
||||
if (contrib != null && contrib.getState() == Contribution.STATE_COMPLETED) {
|
||||
String pageContent;
|
||||
try {
|
||||
pageContent = mwApi.revisionsByFilename(contrib.getFilename());
|
||||
} catch (IOException e) {
|
||||
Timber.d("Network messed up on modifications sync!");
|
||||
continue;
|
||||
}
|
||||
|
||||
Timber.d("Page content is %s", pageContent);
|
||||
String processedPageContent = sequence.executeModifications(contrib.getFilename(), pageContent);
|
||||
|
||||
String editResult;
|
||||
try {
|
||||
editResult = mwApi.edit(editToken, processedPageContent, contrib.getFilename(), sequence.getEditSummary());
|
||||
} catch (IOException e) {
|
||||
Timber.d("Network messed up on modifications sync!");
|
||||
continue;
|
||||
}
|
||||
|
||||
Timber.d("Response is %s", editResult);
|
||||
|
||||
if (!"Success".equals(editResult)) {
|
||||
// FIXME: Log this somewhere else
|
||||
Timber.d("Non success result! %s", editResult);
|
||||
} else {
|
||||
modifierSequenceDao.delete(sequence);
|
||||
}
|
||||
}
|
||||
allModifications.moveToNext();
|
||||
}
|
||||
} finally {
|
||||
if (contributionsClient != null) {
|
||||
contributionsClient.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isNullOrWhiteSpace(String value) {
|
||||
return value == null || value.trim().isEmpty();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
package fr.free.nrw.commons.modifications;
|
||||
|
||||
import android.app.Service;
|
||||
import android.content.Intent;
|
||||
import android.os.IBinder;
|
||||
|
||||
public class ModificationsSyncService extends Service {
|
||||
|
||||
private static final Object sSyncAdapterLock = new Object();
|
||||
|
||||
private static ModificationsSyncAdapter sSyncAdapter = null;
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
synchronized (sSyncAdapterLock) {
|
||||
if (sSyncAdapter == null) {
|
||||
sSyncAdapter = new ModificationsSyncAdapter(this, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return sSyncAdapter.getSyncAdapterBinder();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,64 +0,0 @@
|
|||
package fr.free.nrw.commons.modifications;
|
||||
|
||||
import android.net.Uri;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
public class ModifierSequence {
|
||||
private Uri mediaUri;
|
||||
private ArrayList<PageModifier> modifiers;
|
||||
private Uri contentUri;
|
||||
|
||||
public ModifierSequence(Uri mediaUri) {
|
||||
this.mediaUri = mediaUri;
|
||||
modifiers = new ArrayList<>();
|
||||
}
|
||||
|
||||
ModifierSequence(Uri mediaUri, JSONObject data) {
|
||||
this(mediaUri);
|
||||
JSONArray modifiersJSON = data.optJSONArray("modifiers");
|
||||
for (int i = 0; i < modifiersJSON.length(); i++) {
|
||||
modifiers.add(PageModifier.fromJSON(modifiersJSON.optJSONObject(i)));
|
||||
}
|
||||
}
|
||||
|
||||
Uri getMediaUri() {
|
||||
return mediaUri;
|
||||
}
|
||||
|
||||
public void queueModifier(PageModifier modifier) {
|
||||
modifiers.add(modifier);
|
||||
}
|
||||
|
||||
String executeModifications(String pageName, String pageContents) {
|
||||
for (PageModifier modifier: modifiers) {
|
||||
pageContents = modifier.doModification(pageName, pageContents);
|
||||
}
|
||||
return pageContents;
|
||||
}
|
||||
|
||||
String getEditSummary() {
|
||||
StringBuilder editSummary = new StringBuilder();
|
||||
for (PageModifier modifier: modifiers) {
|
||||
editSummary.append(modifier.getEditSummary()).append(" ");
|
||||
}
|
||||
editSummary.append("Using [[COM:MOA|Commons Mobile App]]");
|
||||
return editSummary.toString();
|
||||
}
|
||||
|
||||
ArrayList<PageModifier> getModifiers() {
|
||||
return modifiers;
|
||||
}
|
||||
|
||||
Uri getContentUri() {
|
||||
return contentUri;
|
||||
}
|
||||
|
||||
void setContentUri(Uri contentUri) {
|
||||
this.contentUri = contentUri;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,124 +0,0 @@
|
|||
package fr.free.nrw.commons.modifications;
|
||||
|
||||
import android.content.ContentProviderClient;
|
||||
import android.content.ContentValues;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.net.Uri;
|
||||
import android.os.RemoteException;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import javax.inject.Provider;
|
||||
|
||||
public class ModifierSequenceDao {
|
||||
|
||||
private final Provider<ContentProviderClient> clientProvider;
|
||||
|
||||
@Inject
|
||||
public ModifierSequenceDao(@Named("modification") Provider<ContentProviderClient> clientProvider) {
|
||||
this.clientProvider = clientProvider;
|
||||
}
|
||||
|
||||
public void save(ModifierSequence sequence) {
|
||||
ContentProviderClient db = clientProvider.get();
|
||||
try {
|
||||
if (sequence.getContentUri() == null) {
|
||||
sequence.setContentUri(db.insert(ModificationsContentProvider.BASE_URI, toContentValues(sequence)));
|
||||
} else {
|
||||
db.update(sequence.getContentUri(), toContentValues(sequence), null, null);
|
||||
}
|
||||
} catch (RemoteException e) {
|
||||
throw new RuntimeException(e);
|
||||
} finally {
|
||||
db.release();
|
||||
}
|
||||
}
|
||||
|
||||
public void delete(ModifierSequence sequence) {
|
||||
ContentProviderClient db = clientProvider.get();
|
||||
try {
|
||||
db.delete(sequence.getContentUri(), null, null);
|
||||
} catch (RemoteException e) {
|
||||
throw new RuntimeException(e);
|
||||
} finally {
|
||||
db.release();
|
||||
}
|
||||
}
|
||||
|
||||
ModifierSequence fromCursor(Cursor cursor) {
|
||||
// Hardcoding column positions!
|
||||
ModifierSequence ms;
|
||||
try {
|
||||
ms = new ModifierSequence(Uri.parse(cursor.getString(cursor.getColumnIndex(Table.COLUMN_MEDIA_URI))),
|
||||
new JSONObject(cursor.getString(cursor.getColumnIndex(Table.COLUMN_DATA))));
|
||||
} catch (JSONException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
ms.setContentUri( ModificationsContentProvider.uriForId(cursor.getInt(cursor.getColumnIndex(Table.COLUMN_ID))));
|
||||
|
||||
return ms;
|
||||
}
|
||||
|
||||
private JSONObject toJSON(ModifierSequence sequence) {
|
||||
JSONObject data = new JSONObject();
|
||||
try {
|
||||
JSONArray modifiersJSON = new JSONArray();
|
||||
for (PageModifier modifier: sequence.getModifiers()) {
|
||||
modifiersJSON.put(modifier.toJSON());
|
||||
}
|
||||
data.put("modifiers", modifiersJSON);
|
||||
return data;
|
||||
} catch (JSONException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private ContentValues toContentValues(ModifierSequence sequence) {
|
||||
ContentValues cv = new ContentValues();
|
||||
cv.put(Table.COLUMN_MEDIA_URI, sequence.getMediaUri().toString());
|
||||
cv.put(Table.COLUMN_DATA, toJSON(sequence).toString());
|
||||
return cv;
|
||||
}
|
||||
|
||||
public static class Table {
|
||||
static final String TABLE_NAME = "modifications";
|
||||
|
||||
static final String COLUMN_ID = "_id";
|
||||
static final String COLUMN_MEDIA_URI = "mediauri";
|
||||
static final String COLUMN_DATA = "data";
|
||||
|
||||
// NOTE! KEEP IN SAME ORDER AS THEY ARE DEFINED UP THERE. HELPS HARD CODE COLUMN INDICES.
|
||||
public static final String[] ALL_FIELDS = {
|
||||
COLUMN_ID,
|
||||
COLUMN_MEDIA_URI,
|
||||
COLUMN_DATA
|
||||
};
|
||||
|
||||
static final String DROP_TABLE_STATEMENT = "DROP TABLE IF EXISTS " + TABLE_NAME;
|
||||
|
||||
static final String CREATE_TABLE_STATEMENT = "CREATE TABLE " + TABLE_NAME + " ("
|
||||
+ "_id INTEGER PRIMARY KEY,"
|
||||
+ "mediauri STRING,"
|
||||
+ "data STRING"
|
||||
+ ");";
|
||||
|
||||
public static void onCreate(SQLiteDatabase db) {
|
||||
db.execSQL(CREATE_TABLE_STATEMENT);
|
||||
}
|
||||
|
||||
public static void onUpdate(SQLiteDatabase db, int from, int to) {
|
||||
db.execSQL(DROP_TABLE_STATEMENT);
|
||||
onCreate(db);
|
||||
}
|
||||
|
||||
public static void onDelete(SQLiteDatabase db) {
|
||||
db.execSQL(DROP_TABLE_STATEMENT);
|
||||
onCreate(db);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
package fr.free.nrw.commons.modifications;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
public abstract class PageModifier {
|
||||
|
||||
public static PageModifier fromJSON(JSONObject data) {
|
||||
String name = data.optString("name");
|
||||
if (name.equals(CategoryModifier.MODIFIER_NAME)) {
|
||||
return new CategoryModifier(data.optJSONObject("data"));
|
||||
} else if (name.equals(TemplateRemoveModifier.MODIFIER_NAME)) {
|
||||
return new TemplateRemoveModifier(data.optJSONObject("data"));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected String name;
|
||||
protected JSONObject params;
|
||||
|
||||
protected PageModifier(String name) {
|
||||
this.name = name;
|
||||
params = new JSONObject();
|
||||
}
|
||||
|
||||
public abstract String doModification(String pageName, String pageContents);
|
||||
|
||||
public abstract String getEditSummary();
|
||||
|
||||
public JSONObject toJSON() {
|
||||
JSONObject data = new JSONObject();
|
||||
try {
|
||||
data.putOpt("name", name);
|
||||
data.put("data", params);
|
||||
} catch (JSONException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,94 +0,0 @@
|
|||
package fr.free.nrw.commons.modifications;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class TemplateRemoveModifier extends PageModifier {
|
||||
|
||||
public static final String MODIFIER_NAME = "TemplateRemoverModifier";
|
||||
|
||||
public static final String PARAM_TEMPLATE_NAME = "template";
|
||||
|
||||
public static final Pattern PATTERN_TEMPLATE_OPEN = Pattern.compile("\\{\\{");
|
||||
public static final Pattern PATTERN_TEMPLATE_CLOSE = Pattern.compile("\\}\\}");
|
||||
|
||||
public TemplateRemoveModifier(String templateName) {
|
||||
super(MODIFIER_NAME);
|
||||
try {
|
||||
params.putOpt(PARAM_TEMPLATE_NAME, templateName);
|
||||
} catch (JSONException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public TemplateRemoveModifier(JSONObject data) {
|
||||
super(MODIFIER_NAME);
|
||||
this.params = data;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String doModification(String pageName, String pageContents) {
|
||||
String templateRawName = params.optString(PARAM_TEMPLATE_NAME);
|
||||
// Wikitext title normalizing rules. Spaces and _ equivalent
|
||||
// They also 'condense' - any number of them reduce to just one (just like HTML)
|
||||
String templateNormalized = templateRawName.trim().replaceAll("(\\s|_)+", "(\\s|_)+");
|
||||
|
||||
// Not supporting {{ inside <nowiki> and HTML comments yet
|
||||
// (Thanks to marktraceur for reminding me of the HTML comments exception)
|
||||
Pattern templateStartPattern = Pattern.compile("\\{\\{" + templateNormalized, Pattern.CASE_INSENSITIVE);
|
||||
Matcher matcher = templateStartPattern.matcher(pageContents);
|
||||
|
||||
while (matcher.find()) {
|
||||
int braceCount = 1;
|
||||
int startIndex = matcher.start();
|
||||
int curIndex = matcher.end();
|
||||
Matcher openMatch = PATTERN_TEMPLATE_OPEN.matcher(pageContents);
|
||||
Matcher closeMatch = PATTERN_TEMPLATE_CLOSE.matcher(pageContents);
|
||||
|
||||
while (curIndex < pageContents.length()) {
|
||||
boolean openFound = openMatch.find(curIndex);
|
||||
boolean closeFound = closeMatch.find(curIndex);
|
||||
|
||||
if (openFound && (!closeFound || openMatch.start() < closeMatch.start())) {
|
||||
braceCount++;
|
||||
curIndex = openMatch.end();
|
||||
} else if (closeFound) {
|
||||
braceCount--;
|
||||
curIndex = closeMatch.end();
|
||||
} else if (braceCount > 0) {
|
||||
// The template never closes, so...remove nothing
|
||||
curIndex = startIndex;
|
||||
break;
|
||||
}
|
||||
|
||||
if (braceCount == 0) {
|
||||
// The braces have all been closed!
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Strip trailing whitespace
|
||||
while (curIndex < pageContents.length()) {
|
||||
if (pageContents.charAt(curIndex) == ' ' || pageContents.charAt(curIndex) == '\n') {
|
||||
curIndex++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// I am so going to hell for this, sigh
|
||||
pageContents = pageContents.substring(0, startIndex) + pageContents.substring(curIndex);
|
||||
matcher = templateStartPattern.matcher(pageContents);
|
||||
}
|
||||
|
||||
return pageContents;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getEditSummary() {
|
||||
return "Removed template " + params.optString(PARAM_TEMPLATE_NAME) + ".";
|
||||
}
|
||||
}
|
||||
|
|
@ -1,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");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
;
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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 >>>>>>>>>>>>");
|
||||
}
|
||||
}
|
||||
|
|
@ -46,15 +46,11 @@ import timber.log.Timber;
|
|||
public class OkHttpJsonApiClient {
|
||||
private static final String THUMB_SIZE = "640";
|
||||
|
||||
public static final Type mapType = new TypeToken<Map<String, String>>() {
|
||||
}.getType();
|
||||
|
||||
private final OkHttpClient okHttpClient;
|
||||
private final HttpUrl wikiMediaToolforgeUrl;
|
||||
private final String sparqlQueryUrl;
|
||||
private final String campaignsUrl;
|
||||
private final String commonsBaseUrl;
|
||||
private final JsonKvStore defaultKvStore;
|
||||
private Gson gson;
|
||||
|
||||
|
||||
|
|
@ -64,14 +60,12 @@ public class OkHttpJsonApiClient {
|
|||
String sparqlQueryUrl,
|
||||
String campaignsUrl,
|
||||
String commonsBaseUrl,
|
||||
JsonKvStore defaultKvStore,
|
||||
Gson gson) {
|
||||
this.okHttpClient = okHttpClient;
|
||||
this.wikiMediaToolforgeUrl = wikiMediaToolforgeUrl;
|
||||
this.sparqlQueryUrl = sparqlQueryUrl;
|
||||
this.campaignsUrl = campaignsUrl;
|
||||
this.commonsBaseUrl = commonsBaseUrl;
|
||||
this.defaultKvStore = defaultKvStore;
|
||||
this.gson = gson;
|
||||
}
|
||||
|
||||
|
|
@ -234,56 +228,6 @@ public class OkHttpJsonApiClient {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* The method returns the picture of the day
|
||||
*
|
||||
* @return Media object corresponding to the picture of the day
|
||||
*/
|
||||
@Nullable
|
||||
public Single<Media> getPictureOfTheDay() {
|
||||
String date = CommonsDateUtil.getIso8601DateFormatShort().format(new Date());
|
||||
Timber.d("Current date is %s", date);
|
||||
String template = "Template:Potd/" + date;
|
||||
return getMedia(template, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches Media object from the imageInfo API
|
||||
*
|
||||
* @param titles the tiles to be searched for. Can be filename or template name
|
||||
* @param useGenerator specifies if a image generator parameter needs to be passed or not
|
||||
* @return
|
||||
*/
|
||||
public Single<Media> getMedia(String titles, boolean useGenerator) {
|
||||
HttpUrl.Builder urlBuilder = HttpUrl
|
||||
.parse(commonsBaseUrl)
|
||||
.newBuilder()
|
||||
.addQueryParameter("action", "query")
|
||||
.addQueryParameter("format", "json")
|
||||
.addQueryParameter("formatversion", "2")
|
||||
.addQueryParameter("titles", titles);
|
||||
|
||||
if (useGenerator) {
|
||||
urlBuilder.addQueryParameter("generator", "images");
|
||||
}
|
||||
|
||||
Request request = new Request.Builder()
|
||||
.url(appendMediaProperties(urlBuilder).build())
|
||||
.build();
|
||||
|
||||
return Single.fromCallable(() -> {
|
||||
Response response = okHttpClient.newCall(request).execute();
|
||||
if (response.body() != null && response.isSuccessful()) {
|
||||
String json = response.body().string();
|
||||
MwQueryResponse mwQueryPage = gson.fromJson(json, MwQueryResponse.class);
|
||||
if (mwQueryPage.success() && mwQueryPage.query().firstPage() != null) {
|
||||
return Media.from(mwQueryPage.query().firstPage());
|
||||
}
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Whenever imageInfo is fetched, these common properties can be specified for the API call
|
||||
* https://www.mediawiki.org/wiki/API:Imageinfo
|
||||
|
|
@ -304,124 +248,4 @@ public class OkHttpJsonApiClient {
|
|||
|
||||
return builder;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method takes the keyword and queryType as input and returns a list of Media objects filtered using image generator query
|
||||
* It uses the generator query API to get the images searched using a query, 10 at a time.
|
||||
* @param queryType queryType can be "search" OR "category"
|
||||
* @param keyword the search keyword. Can be either category name or search query
|
||||
* @return
|
||||
*/
|
||||
@Nullable
|
||||
public Single<List<Media>> getMediaList(String queryType, String keyword) {
|
||||
HttpUrl.Builder urlBuilder = HttpUrl
|
||||
.parse(commonsBaseUrl)
|
||||
.newBuilder()
|
||||
.addQueryParameter("action", "query")
|
||||
.addQueryParameter("format", "json")
|
||||
.addQueryParameter("formatversion", "2");
|
||||
|
||||
|
||||
if (queryType.equals("search")) {
|
||||
appendSearchParam(keyword, urlBuilder);
|
||||
} else {
|
||||
appendCategoryParams(keyword, urlBuilder);
|
||||
}
|
||||
|
||||
appendQueryContinueValues(keyword, urlBuilder);
|
||||
|
||||
Request request = new Request.Builder()
|
||||
.url(appendMediaProperties(urlBuilder).build())
|
||||
.build();
|
||||
|
||||
return Single.fromCallable(() -> {
|
||||
Response response = okHttpClient.newCall(request).execute();
|
||||
List<Media> mediaList = new ArrayList<>();
|
||||
if (response.body() != null && response.isSuccessful()) {
|
||||
String json = response.body().string();
|
||||
MwQueryResponse mwQueryResponse = gson.fromJson(json, MwQueryResponse.class);
|
||||
if (null == mwQueryResponse
|
||||
|| null == mwQueryResponse.query()
|
||||
|| null == mwQueryResponse.query().pages()) {
|
||||
return mediaList;
|
||||
}
|
||||
putContinueValues(keyword, mwQueryResponse.continuation());
|
||||
|
||||
List<MwQueryPage> pages = mwQueryResponse.query().pages();
|
||||
for (MwQueryPage page : pages) {
|
||||
Media media = Media.from(page);
|
||||
if (media != null) {
|
||||
mediaList.add(media);
|
||||
}
|
||||
}
|
||||
}
|
||||
return mediaList;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Append params for search query.
|
||||
*
|
||||
* @param query the search query to be sent to the API
|
||||
* @param urlBuilder builder for HttpUrl
|
||||
*/
|
||||
private void appendSearchParam(String query, HttpUrl.Builder urlBuilder) {
|
||||
urlBuilder.addQueryParameter("generator", "search")
|
||||
.addQueryParameter("gsrwhat", "text")
|
||||
.addQueryParameter("gsrnamespace", "6")
|
||||
.addQueryParameter("gsrlimit", "25")
|
||||
.addQueryParameter("gsrsearch", query);
|
||||
}
|
||||
|
||||
/**
|
||||
* It takes a urlBuilder and appends all the continue values as query parameters
|
||||
*
|
||||
* @param query
|
||||
* @param urlBuilder
|
||||
*/
|
||||
private void appendQueryContinueValues(String query, HttpUrl.Builder urlBuilder) {
|
||||
Map<String, String> continueValues = getContinueValues(query);
|
||||
if (continueValues != null && continueValues.size() > 0) {
|
||||
for (Map.Entry<String, String> entry : continueValues.entrySet()) {
|
||||
urlBuilder.addQueryParameter(entry.getKey(), entry.getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Append parameters for category image generator
|
||||
*
|
||||
* @param categoryName name of the category
|
||||
* @param urlBuilder HttpUrl builder
|
||||
*/
|
||||
private void appendCategoryParams(String categoryName, HttpUrl.Builder urlBuilder) {
|
||||
urlBuilder.addQueryParameter("generator", "categorymembers")
|
||||
.addQueryParameter("gcmtype", "file")
|
||||
.addQueryParameter("gcmtitle", categoryName)
|
||||
.addQueryParameter("gcmsort", "timestamp")//property to sort by;timestamp
|
||||
.addQueryParameter("gcmdir", "desc")//in which direction to sort;descending
|
||||
.addQueryParameter("gcmlimit", "10");
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the continue values for action=query
|
||||
* These values are sent to the server in the subsequent call to fetch results after this point
|
||||
*
|
||||
* @param keyword
|
||||
* @param values
|
||||
*/
|
||||
private void putContinueValues(String keyword, Map<String, String> values) {
|
||||
defaultKvStore.putJson("query_continue_" + keyword, values);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a map of continue values from shared preferences.
|
||||
* These values are appended to the next API call
|
||||
*
|
||||
* @param keyword
|
||||
* @return
|
||||
*/
|
||||
private Map<String, String> getContinueValues(String keyword) {
|
||||
return defaultKvStore.getJson("query_continue_" + keyword, mapType);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
63
app/src/main/java/fr/free/nrw/commons/mwapi/UserClient.java
Normal file
63
app/src/main/java/fr/free/nrw/commons/mwapi/UserClient.java
Normal 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();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -1,5 +1,9 @@
|
|||
package fr.free.nrw.commons.notification;
|
||||
|
||||
import org.wikipedia.util.DateUtil;
|
||||
|
||||
import fr.free.nrw.commons.utils.CommonsDateUtil;
|
||||
|
||||
/**
|
||||
* Created by root on 18.12.2017.
|
||||
*/
|
||||
|
|
@ -8,33 +12,44 @@ public class Notification {
|
|||
public NotificationType notificationType;
|
||||
public String notificationText;
|
||||
public String date;
|
||||
public String description;
|
||||
public String link;
|
||||
public String iconUrl;
|
||||
public String dateWithYear;
|
||||
public String notificationId;
|
||||
|
||||
public Notification(NotificationType notificationType, String notificationText, String date, String description, String link, String iconUrl, String dateWithYear, String notificationId) {
|
||||
public Notification(NotificationType notificationType,
|
||||
String notificationText,
|
||||
String date,
|
||||
String link,
|
||||
String iconUrl,
|
||||
String notificationId) {
|
||||
this.notificationType = notificationType;
|
||||
this.notificationText = notificationText;
|
||||
this.date = date;
|
||||
this.description = description;
|
||||
this.link = link;
|
||||
this.iconUrl = iconUrl;
|
||||
this.dateWithYear = dateWithYear;
|
||||
this.notificationId=notificationId;
|
||||
}
|
||||
|
||||
public static Notification from(org.wikipedia.notifications.Notification wikiNotification) {
|
||||
org.wikipedia.notifications.Notification.Contents contents = wikiNotification.getContents();
|
||||
String notificationLink = contents == null || contents.getLinks() == null
|
||||
|| contents.getLinks().getPrimary() == null ? "" : contents.getLinks().getPrimary().getUrl();
|
||||
return new Notification(NotificationType.UNKNOWN,
|
||||
contents == null ? "" : contents.getCompactHeader(),
|
||||
DateUtil.getMonthOnlyDateString(wikiNotification.getTimestamp()),
|
||||
notificationLink,
|
||||
"",
|
||||
String.valueOf(wikiNotification.id()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Notification" +
|
||||
"notificationType='" + notificationType + '\'' +
|
||||
", notificationText='" + notificationText + '\'' +
|
||||
", date='" + date + '\'' +
|
||||
", description='" + description + '\'' +
|
||||
", link='" + link + '\'' +
|
||||
", iconUrl='" + iconUrl + '\'' +
|
||||
", dateWithYear=" + dateWithYear +
|
||||
", notificationId='" + notificationId + '\'' +
|
||||
'}';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,11 +5,6 @@ import android.content.Context;
|
|||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
import androidx.recyclerview.widget.DividerItemDecoration;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
|
|
@ -19,10 +14,17 @@ import android.widget.RelativeLayout;
|
|||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||
import androidx.recyclerview.widget.DividerItemDecoration;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
import com.pedrogomez.renderers.RVRendererAdapter;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Callable;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
|
|
@ -34,7 +36,9 @@ import fr.free.nrw.commons.theme.NavigationBaseActivity;
|
|||
import fr.free.nrw.commons.utils.NetworkUtils;
|
||||
import fr.free.nrw.commons.utils.ViewUtil;
|
||||
import io.reactivex.Observable;
|
||||
import io.reactivex.ObservableSource;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import timber.log.Timber;
|
||||
|
||||
|
|
@ -78,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 -> {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,45 @@
|
|||
package fr.free.nrw.commons.notification;
|
||||
|
||||
import org.wikipedia.csrf.CsrfTokenClient;
|
||||
import org.wikipedia.dataclient.Service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import io.reactivex.Observable;
|
||||
import io.reactivex.Single;
|
||||
|
||||
import static fr.free.nrw.commons.di.NetworkingModule.NAMED_COMMONS_CSRF;
|
||||
|
||||
@Singleton
|
||||
public class NotificationClient {
|
||||
|
||||
private final Service service;
|
||||
private final CsrfTokenClient csrfTokenClient;
|
||||
|
||||
@Inject
|
||||
public NotificationClient(@Named("commons-service") Service service, @Named(NAMED_COMMONS_CSRF) CsrfTokenClient csrfTokenClient) {
|
||||
this.service = service;
|
||||
this.csrfTokenClient = csrfTokenClient;
|
||||
}
|
||||
|
||||
public Single<List<Notification>> getNotifications(boolean archived) {
|
||||
return service.getAllNotifications("wikidatawiki|commonswiki|enwiki", archived ? "read" : "!read", null)
|
||||
.map(mwQueryResponse -> mwQueryResponse.query().notifications().list())
|
||||
.flatMap(Observable::fromIterable)
|
||||
.map(notification -> Notification.from(notification))
|
||||
.toList();
|
||||
}
|
||||
|
||||
public Observable<Boolean> markNotificationAsRead(String notificationId) {
|
||||
try {
|
||||
return service.markRead(csrfTokenClient.getTokenBlocking(), notificationId, "")
|
||||
.map(mwQueryResponse -> mwQueryResponse.success());
|
||||
} catch (Throwable throwable) {
|
||||
return Observable.just(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +1,12 @@
|
|||
package fr.free.nrw.commons.notification;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import fr.free.nrw.commons.auth.SessionManager;
|
||||
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
||||
import io.reactivex.Observable;
|
||||
import io.reactivex.Single;
|
||||
|
||||
/**
|
||||
* Created by root on 19.12.2017.
|
||||
|
|
@ -16,27 +14,19 @@ import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
|||
@Singleton
|
||||
public class NotificationController {
|
||||
|
||||
private MediaWikiApi mediaWikiApi;
|
||||
private SessionManager sessionManager;
|
||||
private NotificationClient notificationClient;
|
||||
|
||||
|
||||
@Inject
|
||||
public NotificationController(MediaWikiApi mediaWikiApi, SessionManager sessionManager) {
|
||||
this.mediaWikiApi = mediaWikiApi;
|
||||
this.sessionManager = sessionManager;
|
||||
public NotificationController(NotificationClient notificationClient) {
|
||||
this.notificationClient = notificationClient;
|
||||
}
|
||||
|
||||
public List<Notification> getNotifications(boolean archived) throws IOException {
|
||||
if (mediaWikiApi.validateLogin()) {
|
||||
return mediaWikiApi.getNotifications(archived);
|
||||
} else {
|
||||
Boolean authTokenValidated = sessionManager.revalidateAuthToken();
|
||||
if (authTokenValidated != null && authTokenValidated) {
|
||||
return mediaWikiApi.getNotifications(archived);
|
||||
}
|
||||
}
|
||||
return new ArrayList<>();
|
||||
public 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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
package fr.free.nrw.commons.upload;
|
||||
|
||||
public class UploadResponse {
|
||||
private final UploadResult upload;
|
||||
|
||||
public UploadResponse(UploadResult upload) {
|
||||
this.upload = upload;
|
||||
}
|
||||
|
||||
public UploadResult getUpload() {
|
||||
return upload;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
package fr.free.nrw.commons.upload;
|
||||
|
||||
import org.wikipedia.gallery.ImageInfo;
|
||||
|
||||
public class UploadResult {
|
||||
private final String result;
|
||||
private final String filekey;
|
||||
private final String filename;
|
||||
private final String sessionkey;
|
||||
private final ImageInfo imageinfo;
|
||||
|
||||
public UploadResult(String result, String filekey, String filename, String sessionkey, ImageInfo imageinfo) {
|
||||
this.result = result;
|
||||
this.filekey = filekey;
|
||||
this.filename = filename;
|
||||
this.sessionkey = sessionkey;
|
||||
this.imageinfo = imageinfo;
|
||||
}
|
||||
|
||||
public String getResult() {
|
||||
return result;
|
||||
}
|
||||
|
||||
public String getFilekey() {
|
||||
return filekey;
|
||||
}
|
||||
|
||||
public String getSessionkey() {
|
||||
return sessionkey;
|
||||
}
|
||||
|
||||
public ImageInfo getImageinfo() {
|
||||
return imageinfo;
|
||||
}
|
||||
|
||||
public String getFilename() {
|
||||
return filename;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import androidx.annotation.Nullable;
|
|||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.contributions.MainActivity;
|
||||
import fr.free.nrw.commons.di.ApplicationlessInjection;
|
||||
import fr.free.nrw.commons.media.MediaClient;
|
||||
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
|
|
@ -42,7 +43,7 @@ public class PicOfDayAppWidget extends AppWidgetProvider {
|
|||
|
||||
private CompositeDisposable compositeDisposable = new CompositeDisposable();
|
||||
|
||||
@Inject OkHttpJsonApiClient okHttpJsonApiClient;
|
||||
@Inject MediaClient mediaClient;
|
||||
|
||||
void updateAppWidget(Context context, AppWidgetManager appWidgetManager, int appWidgetId) {
|
||||
RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.pic_of_day_app_widget);
|
||||
|
|
@ -67,7 +68,7 @@ public class PicOfDayAppWidget extends AppWidgetProvider {
|
|||
RemoteViews views,
|
||||
AppWidgetManager appWidgetManager,
|
||||
int appWidgetId) {
|
||||
compositeDisposable.add(okHttpJsonApiClient.getPictureOfTheDay()
|
||||
compositeDisposable.add(mediaClient.getPictureOfTheDay()
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,40 @@
|
|||
package fr.free.nrw.commons.wikidata;
|
||||
|
||||
import org.wikipedia.csrf.CsrfTokenClient;
|
||||
import org.wikipedia.dataclient.Service;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import io.reactivex.Observable;
|
||||
|
||||
import static fr.free.nrw.commons.di.NetworkingModule.NAMED_WIKI_DATA_CSRF;
|
||||
|
||||
@Singleton
|
||||
public class WikidataClient {
|
||||
|
||||
private final Service service;
|
||||
private final CsrfTokenClient csrfTokenClient;
|
||||
|
||||
@Inject
|
||||
public WikidataClient(@Named("wikidata-service") Service service,
|
||||
@Named(NAMED_WIKI_DATA_CSRF) CsrfTokenClient csrfTokenClient) {
|
||||
this.service = service;
|
||||
this.csrfTokenClient = csrfTokenClient;
|
||||
}
|
||||
|
||||
public Observable<Long> createClaim(String entityId, String property, String snaktype, String value) {
|
||||
try {
|
||||
return service.postCreateClaim(entityId, snaktype, property, value, "en", csrfTokenClient.getTokenBlocking())
|
||||
.map(mwPostResponse -> {
|
||||
if (mwPostResponse.getSuccessVal() == 1) {
|
||||
return 1L;
|
||||
}
|
||||
return -1L;
|
||||
});
|
||||
} catch (Throwable throwable) {
|
||||
return Observable.just(-1L);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -10,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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue