mirror of
				https://github.com/commons-app/apps-android-commons.git
				synced 2025-10-31 14:53:59 +01:00 
			
		
		
		
	Merge branch 'main' into fix/5564_Feedback_should_contain_date_and_time
This commit is contained in:
		
						commit
						f37e24158b
					
				
					 87 changed files with 2977 additions and 1008 deletions
				
			
		|  | @ -195,36 +195,4 @@ class MainActivityTest { | |||
|         Espresso.pressBack() | ||||
|         UITestHelper.sleep(1000) | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     fun testLimitedConnectionModeToggle() { | ||||
|         val isEnabled = defaultKvStore | ||||
|             .getBoolean(CommonsApplication.IS_LIMITED_CONNECTION_MODE_ENABLED, false) | ||||
|         Espresso.onView( | ||||
|             Matchers.allOf( | ||||
|                 ViewMatchers.withId(R.id.toggle_limited_connection_mode), | ||||
|                 childAtPosition( | ||||
|                     childAtPosition( | ||||
|                         ViewMatchers.withId(R.id.toolbar), | ||||
|                         1 | ||||
|                     ), | ||||
|                     0 | ||||
|                 ), | ||||
|                 ViewMatchers.isDisplayed() | ||||
|             ) | ||||
|         ).perform(ViewActions.click()) | ||||
|         UITestHelper.sleep(1000) | ||||
|         if (isEnabled) { | ||||
|             Assert.assertFalse( | ||||
|                 defaultKvStore | ||||
|                     .getBoolean(CommonsApplication.IS_LIMITED_CONNECTION_MODE_ENABLED, false) | ||||
|             ) | ||||
|         } else { | ||||
|             Assert.assertTrue( | ||||
|                 defaultKvStore | ||||
|                     .getBoolean(CommonsApplication.IS_LIMITED_CONNECTION_MODE_ENABLED, false) | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | @ -1,6 +1,7 @@ | |||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <manifest xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|   xmlns:tools="http://schemas.android.com/tools"> | ||||
| 
 | ||||
|   <uses-permission android:name="android.permission.INTERNET" /> | ||||
|   <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> | ||||
|   <uses-permission android:name="android.permission.READ_SYNC_SETTINGS" /> | ||||
|  | @ -13,66 +14,65 @@ | |||
|   <uses-permission android:name="android.permission.GET_ACCOUNTS" /> | ||||
|   <uses-permission android:name="android.permission.USE_CREDENTIALS" /> | ||||
|   <uses-permission android:name="android.permission.MANAGE_ACCOUNTS" /> | ||||
|   <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/> | ||||
|   <uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/> | ||||
|   <uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> | ||||
|   <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" /> | ||||
|   <uses-permission android:name="com.google.android.apps.photos.permission.GOOGLE_PHOTOS" /> | ||||
|   <uses-permission android:name="android.permission.SET_WALLPAPER"/> | ||||
|   <uses-permission android:name="android.permission.SET_WALLPAPER" /> | ||||
|   <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> | ||||
|   <uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION"/> | ||||
|   <uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" /> | ||||
| 
 | ||||
|   <queries> | ||||
| 
 | ||||
|     <!-- Browser --> | ||||
|     <intent> | ||||
|       <action android:name="android.intent.action.VIEW" /> | ||||
| 
 | ||||
|       <category android:name="android.intent.category.BROWSABLE" /> | ||||
| 
 | ||||
|       <data android:scheme="https" /> | ||||
|     </intent> | ||||
|     <!-- Google Maps --> | ||||
|     <package android:name="com.google.android.apps.maps" /> | ||||
|   </queries> | ||||
| 
 | ||||
| 
 | ||||
|   <!-- Needed only if your app targets Android 5.0 (API level 21) or higher. --> | ||||
|   </queries> <!-- Needed only if your app targets Android 5.0 (API level 21) or higher. --> | ||||
|   <uses-feature android:name="android.hardware.location.gps" /> | ||||
| 
 | ||||
|   <application | ||||
|     android:name=".CommonsApplication" | ||||
|     android:appComponentFactory="commons" | ||||
|     android:icon="@mipmap/ic_launcher" | ||||
|     android:label="@string/app_name" | ||||
|     android:theme="@style/LightAppTheme" | ||||
|     android:largeHeap="true" | ||||
|     android:requestLegacyExternalStorage="true" | ||||
|     android:supportsRtl="true" | ||||
|     tools:replace="android:appComponentFactory" | ||||
|     android:appComponentFactory="commons" | ||||
|     android:requestLegacyExternalStorage = "true" | ||||
|     tools:ignore="GoogleAppIndexingWarning"> | ||||
| 
 | ||||
|     android:theme="@style/LightAppTheme" | ||||
|     tools:ignore="GoogleAppIndexingWarning" | ||||
|     tools:replace="android:appComponentFactory"> | ||||
|     <activity | ||||
|       android:name=".nearby.WikidataFeedback" | ||||
|       android:exported="false" /> | ||||
| 
 | ||||
|     <activity | ||||
|       android:theme="@style/EditActivityTheme" | ||||
|       android:name=".upload.UploadProgressActivity" | ||||
|       android:exported="false" /> | ||||
|     <activity | ||||
|       android:name=".description.DescriptionEditActivity" | ||||
|       android:exported="true" /> | ||||
| 
 | ||||
|       android:exported="true" | ||||
|       android:theme="@style/EditActivityTheme" /> | ||||
|     <activity | ||||
|       android:name=".edit.EditActivity" | ||||
|       android:exported="false" /> | ||||
| 
 | ||||
|     <activity android:name="org.acra.dialog.CrashReportDialog" | ||||
|       android:process=":acra" | ||||
|       android:launchMode="singleInstance" | ||||
|     <activity | ||||
|       android:name="org.acra.dialog.CrashReportDialog" | ||||
|       android:excludeFromRecents="true" | ||||
|       android:finishOnTaskLaunch="true" /> | ||||
| 
 | ||||
|       android:finishOnTaskLaunch="true" | ||||
|       android:launchMode="singleInstance" | ||||
|       android:process=":acra" /> | ||||
|     <activity | ||||
|       android:name=".media.ZoomableActivity" | ||||
|       android:label="Zoomable Activity" | ||||
|       android:configChanges="screenSize|keyboard|orientation" | ||||
|       android:label="Zoomable Activity" | ||||
|       android:parentActivityName=".customselector.ui.selector.CustomSelectorActivity" /> | ||||
| 
 | ||||
|     <activity android:name=".auth.LoginActivity" | ||||
|     <activity | ||||
|       android:name=".auth.LoginActivity" | ||||
|       android:exported="true"> | ||||
|       <intent-filter> | ||||
|         <category android:name="android.intent.category.LAUNCHER" /> | ||||
|  | @ -80,21 +80,19 @@ | |||
|         <action android:name="android.intent.action.MAIN" /> | ||||
|       </intent-filter> | ||||
| 
 | ||||
|       <meta-data android:name="android.app.shortcuts" | ||||
|       <meta-data | ||||
|         android:name="android.app.shortcuts" | ||||
|         android:resource="@xml/shortcuts" /> | ||||
| 
 | ||||
|     </activity> | ||||
|     <activity android:name=".WelcomeActivity" /> | ||||
| 
 | ||||
|     <activity | ||||
|       android:hardwareAccelerated="false" | ||||
|       android:name=".upload.UploadActivity" | ||||
|       android:exported="true" | ||||
|       android:configChanges="orientation|screenSize|keyboard" | ||||
|       android:exported="true" | ||||
|       android:hardwareAccelerated="false" | ||||
|       android:icon="@mipmap/ic_launcher" | ||||
|       android:label="@string/app_name" | ||||
|       android:windowSoftInputMode="adjustResize" | ||||
|       > | ||||
|       android:windowSoftInputMode="adjustResize"> | ||||
|       <intent-filter android:label="@string/intent_share_upload_label"> | ||||
|         <action android:name="android.intent.action.SEND" /> | ||||
| 
 | ||||
|  | @ -114,9 +112,9 @@ | |||
|     </activity> | ||||
|     <activity | ||||
|       android:name=".contributions.MainActivity" | ||||
|       android:configChanges="screenSize|keyboard|orientation" | ||||
|       android:icon="@mipmap/ic_launcher" | ||||
|       android:label="@string/app_name" | ||||
|       android:configChanges="screenSize|keyboard|orientation" /> | ||||
|       android:label="@string/app_name" /> | ||||
|     <activity | ||||
|       android:name=".settings.SettingsActivity" | ||||
|       android:label="@string/title_activity_settings" /> | ||||
|  | @ -124,57 +122,47 @@ | |||
|       android:name=".AboutActivity" | ||||
|       android:label="@string/title_activity_about" | ||||
|       android:parentActivityName=".contributions.MainActivity" /> | ||||
| 
 | ||||
|     <activity | ||||
|       android:name=".auth.SignupActivity" | ||||
|       android:configChanges="orientation|screenLayout|screenSize" | ||||
|       android:label="@string/title_activity_signup" /> | ||||
| 
 | ||||
|     <activity | ||||
|       android:name=".notification.NotificationActivity" | ||||
|       android:label="@string/navigation_item_notification" /> | ||||
| 
 | ||||
|     <activity android:name=".quiz.QuizActivity" | ||||
|       android:label="@string/quiz"/> | ||||
| 
 | ||||
|     <activity android:name=".quiz.QuizResultActivity" | ||||
|       android:label="@string/result"/> | ||||
| 
 | ||||
|     <activity | ||||
|       android:name=".quiz.QuizActivity" | ||||
|       android:label="@string/quiz" /> | ||||
|     <activity | ||||
|       android:name=".quiz.QuizResultActivity" | ||||
|       android:label="@string/result" /> | ||||
|     <activity | ||||
|       android:name=".customselector.ui.selector.CustomSelectorActivity" | ||||
|       android:label="@string/title_activity_custom_selector" | ||||
|       android:configChanges="screenSize|keyboard|orientation" | ||||
|       android:label="@string/title_activity_custom_selector" | ||||
|       android:parentActivityName=".contributions.MainActivity" /> | ||||
| 
 | ||||
|     <activity | ||||
|       android:name=".category.CategoryDetailsActivity" | ||||
|       android:label="@string/title_activity_featured_images" | ||||
|       android:configChanges="screenSize|keyboard|orientation" | ||||
|       android:label="@string/title_activity_featured_images" | ||||
|       android:parentActivityName=".contributions.MainActivity" /> | ||||
| 
 | ||||
|     <activity | ||||
|       android:name=".explore.depictions.WikidataItemDetailsActivity" | ||||
|       android:label="@string/title_activity_featured_images" | ||||
|       android:configChanges="screenSize|keyboard|orientation" | ||||
|       android:label="@string/title_activity_featured_images" | ||||
|       android:parentActivityName=".contributions.MainActivity" /> | ||||
| 
 | ||||
|     <activity | ||||
|       android:name=".explore.SearchActivity" | ||||
|       android:configChanges="orientation|keyboardHidden|screenSize" | ||||
|       android:label="@string/title_activity_search" | ||||
|       android:launchMode="singleTop" | ||||
|       android:configChanges="orientation|keyboardHidden|screenSize" | ||||
|       android:parentActivityName=".contributions.MainActivity" | ||||
|       /> | ||||
| 
 | ||||
|       android:parentActivityName=".contributions.MainActivity" /> | ||||
|     <activity | ||||
|       android:name=".profile.ProfileActivity" | ||||
|       android:configChanges="orientation|screenSize|keyboard" | ||||
|       android:label="@string/Profile" /> | ||||
| 
 | ||||
|     <activity | ||||
|       android:name=".review.ReviewActivity" | ||||
|       android:label="@string/title_activity_review" /> | ||||
| 
 | ||||
|     <activity | ||||
|       android:name=".LocationPicker.LocationPickerActivity" | ||||
|       android:label="Location Picker" /> | ||||
|  | @ -186,11 +174,11 @@ | |||
|       <intent-filter> | ||||
|         <action android:name="android.accounts.AccountAuthenticator" /> | ||||
|       </intent-filter> | ||||
| 
 | ||||
|       <meta-data | ||||
|         android:name="android.accounts.AccountAuthenticator" | ||||
|         android:resource="@xml/authenticator" /> | ||||
|     </service> | ||||
| 
 | ||||
|     <service | ||||
|       android:name="org.acra.sender.SenderService" | ||||
|       android:exported="false" | ||||
|  | @ -205,42 +193,36 @@ | |||
|         android:name="android.support.FILE_PROVIDER_PATHS" | ||||
|         android:resource="@xml/provider_paths" /> | ||||
|     </provider> | ||||
| 
 | ||||
|     <provider | ||||
|       android:name=".category.CategoryContentProvider" | ||||
|       android:authorities="${applicationId}.categories.contentprovider" | ||||
|       android:exported="false" | ||||
|       android:label="@string/provider_categories" | ||||
|       android:syncable="false" /> | ||||
| 
 | ||||
|     <provider | ||||
|       android:name=".explore.recentsearches.RecentSearchesContentProvider" | ||||
|       android:authorities="${applicationId}.explore.recentsearches.contentprovider" | ||||
|       android:exported="false" | ||||
|       android:label="@string/provider_searches" | ||||
|       android:syncable="false" /> | ||||
| 
 | ||||
|     <provider | ||||
|       android:name=".recentlanguages.RecentLanguagesContentProvider" | ||||
|       android:authorities="${applicationId}.recentlanguages.contentprovider" | ||||
|       android:exported="false" | ||||
|       android:label="@string/provider_recent_languages" | ||||
|       android:syncable="false" /> | ||||
| 
 | ||||
|     <provider | ||||
|       android:name=".bookmarks.pictures.BookmarkPicturesContentProvider" | ||||
|       android:authorities="${applicationId}.bookmarks.contentprovider" | ||||
|       android:exported="false" | ||||
|       android:label="@string/provider_bookmarks" | ||||
|       android:syncable="false" /> | ||||
| 
 | ||||
|     <provider | ||||
|       android:name=".bookmarks.locations.BookmarkLocationsContentProvider" | ||||
|       android:authorities="${applicationId}.bookmarks.locations.contentprovider" | ||||
|       android:exported="false" | ||||
|       android:label="@string/provider_bookmarks_location" | ||||
|       android:syncable="false" /> | ||||
| 
 | ||||
|     <provider | ||||
|       android:name=".bookmarks.items.BookmarkItemsContentProvider" | ||||
|       android:authorities="${applicationId}.bookmarks.items.contentprovider" | ||||
|  | @ -248,7 +230,8 @@ | |||
|       android:label="@string/provider_bookmarks_location" | ||||
|       android:syncable="false" /> | ||||
| 
 | ||||
|     <receiver android:name=".widget.PicOfDayAppWidget" | ||||
|     <receiver | ||||
|       android:name=".widget.PicOfDayAppWidget" | ||||
|       android:exported="true"> | ||||
|       <intent-filter> | ||||
|         <action android:name="android.appwidget.action.APPWIDGET_UPDATE" /> | ||||
|  | @ -259,8 +242,9 @@ | |||
|         android:resource="@xml/pic_of_day_app_widget_info" /> | ||||
|     </receiver> | ||||
| 
 | ||||
|     <uses-library android:name="org.apache.http.legacy" android:required="false" /> | ||||
| 
 | ||||
|     <uses-library | ||||
|       android:name="org.apache.http.legacy" | ||||
|       android:required="false" /> | ||||
|   </application> | ||||
| 
 | ||||
| </manifest> | ||||
|  | @ -142,15 +142,7 @@ public class CommonsApplication extends MultiDexApplication { | |||
|     @Inject | ||||
|     ContributionDao contributionDao; | ||||
| 
 | ||||
|     /** | ||||
|      * In-memory list of contributions whose uploads have been paused by the user | ||||
|      */ | ||||
|     public static Map<String, Boolean> pauseUploads = new HashMap<>(); | ||||
| 
 | ||||
|     /** | ||||
|      * In-memory list of uploads that have been cancelled by the user | ||||
|      */ | ||||
|     public static HashSet<String> cancelledUploads = new HashSet<>(); | ||||
|     public static Boolean isPaused = false; | ||||
| 
 | ||||
|     /** | ||||
|      * Used to declare and initialize various components and dependencies | ||||
|  |  | |||
|  | @ -28,6 +28,7 @@ data class Contribution constructor( | |||
|     var dateCreatedSource: String? = null, | ||||
|     var wikidataPlace: WikidataPlace? = null, | ||||
|     var chunkInfo: ChunkInfo? = null, | ||||
|     var errorInfo: String? = null, | ||||
|     /** | ||||
|      * @return array list of entityids for the depictions | ||||
|      */ | ||||
|  | @ -42,6 +43,7 @@ data class Contribution constructor( | |||
|     var dateCreated: Date? = null, | ||||
|     var dateCreatedString: String? = null, | ||||
|     var dateModified: Date? = null, | ||||
|     var dateUploadStarted: Date? = null, | ||||
|     var hasInvalidLocation : Int =  0, | ||||
|     var contentUri: Uri? = null, | ||||
|     var countryCode : String? = null, | ||||
|  | @ -99,7 +101,6 @@ data class Contribution constructor( | |||
|         const val STATE_QUEUED = 2 | ||||
|         const val STATE_IN_PROGRESS = 3 | ||||
|         const val STATE_PAUSED = 4 | ||||
|         const val STATE_QUEUED_LIMITED_CONNECTION_MODE=5 | ||||
| 
 | ||||
|         /** | ||||
|          * Formatting captions to the Wikibase format for sending labels | ||||
|  | @ -127,11 +128,8 @@ data class Contribution constructor( | |||
|         return chunkInfo != null && chunkInfo!!.totalChunks == chunkInfo!!.indexOfNextChunkToUpload | ||||
|     } | ||||
| 
 | ||||
|     fun isPaused(): Boolean { | ||||
|         return CommonsApplication.pauseUploads[pageId] ?: false | ||||
|     fun dateUploadStartedInMillis(): Long { | ||||
|         return dateUploadStarted!!.time | ||||
|     } | ||||
| 
 | ||||
|     fun unpause() { | ||||
|         CommonsApplication.pauseUploads[pageId] = false | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -9,6 +9,10 @@ import android.content.Intent; | |||
| import android.widget.Toast; | ||||
| import androidx.activity.result.ActivityResultLauncher; | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.lifecycle.LiveData; | ||||
| import androidx.paging.DataSource.Factory; | ||||
| import androidx.paging.LivePagedListBuilder; | ||||
| import androidx.paging.PagedList; | ||||
| import fr.free.nrw.commons.R; | ||||
| import fr.free.nrw.commons.filepicker.DefaultCallback; | ||||
| import fr.free.nrw.commons.filepicker.FilePicker; | ||||
|  | @ -25,6 +29,8 @@ import fr.free.nrw.commons.utils.DialogUtil; | |||
| import fr.free.nrw.commons.utils.PermissionUtils; | ||||
| import fr.free.nrw.commons.utils.ViewUtil; | ||||
| import java.util.ArrayList; | ||||
| import java.util.Arrays; | ||||
| import java.util.Collections; | ||||
| import java.util.List; | ||||
| import javax.inject.Inject; | ||||
| import javax.inject.Named; | ||||
|  | @ -39,10 +45,16 @@ public class ContributionController { | |||
|     private boolean isInAppCameraUpload; | ||||
|     public LocationPermissionCallback locationPermissionCallback; | ||||
|     private LocationPermissionsHelper locationPermissionsHelper; | ||||
|     LiveData<PagedList<Contribution>> failedAndPendingContributionList; | ||||
|     LiveData<PagedList<Contribution>> pendingContributionList; | ||||
|     LiveData<PagedList<Contribution>> failedContributionList; | ||||
| 
 | ||||
|     @Inject | ||||
|     LocationServiceManager locationManager; | ||||
| 
 | ||||
|     @Inject | ||||
|     ContributionsRepository repository; | ||||
| 
 | ||||
|     @Inject | ||||
|     public ContributionController(@Named("default_preferences") JsonKvStore defaultKvStore) { | ||||
|         this.defaultKvStore = defaultKvStore; | ||||
|  | @ -115,8 +127,8 @@ public class ContributionController { | |||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Shows a dialog alerting the user about location services being off | ||||
|      * and asking them to turn it on | ||||
|      * Shows a dialog alerting the user about location services being off and asking them to turn it | ||||
|      * on | ||||
|      * TODO: Add a seperate callback in LocationPermissionsHelper for this. | ||||
|      *      Ref: https://github.com/commons-app/apps-android-commons/pull/5494/files#r1510553114 | ||||
|      * | ||||
|  | @ -307,4 +319,60 @@ public class ContributionController { | |||
|         isInAppCameraUpload = false;    // reset the flag for next use | ||||
|         return shareIntent; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Fetches the contributions with the state "IN_PROGRESS", "QUEUED" and "PAUSED" and then it | ||||
|      * populates the `pendingContributionList`. | ||||
|      **/ | ||||
|     void getPendingContributions() { | ||||
|         final PagedList.Config pagedListConfig = | ||||
|             (new PagedList.Config.Builder()) | ||||
|                 .setPrefetchDistance(50) | ||||
|                 .setPageSize(10).build(); | ||||
|         Factory<Integer, Contribution> factory; | ||||
|         factory = repository.fetchContributionsWithStates( | ||||
|             Arrays.asList(Contribution.STATE_IN_PROGRESS, Contribution.STATE_QUEUED, | ||||
|                 Contribution.STATE_PAUSED)); | ||||
| 
 | ||||
|         LivePagedListBuilder livePagedListBuilder = new LivePagedListBuilder(factory, | ||||
|             pagedListConfig); | ||||
|         pendingContributionList = livePagedListBuilder.build(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Fetches the contributions with the state "FAILED" and populates the | ||||
|      * `failedContributionList`. | ||||
|      **/ | ||||
|     void getFailedContributions() { | ||||
|         final PagedList.Config pagedListConfig = | ||||
|             (new PagedList.Config.Builder()) | ||||
|                 .setPrefetchDistance(50) | ||||
|                 .setPageSize(10).build(); | ||||
|         Factory<Integer, Contribution> factory; | ||||
|         factory = repository.fetchContributionsWithStates( | ||||
|             Collections.singletonList(Contribution.STATE_FAILED)); | ||||
| 
 | ||||
|         LivePagedListBuilder livePagedListBuilder = new LivePagedListBuilder(factory, | ||||
|             pagedListConfig); | ||||
|         failedContributionList = livePagedListBuilder.build(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Fetches the contributions with the state "IN_PROGRESS", "QUEUED", "PAUSED" and "FAILED" and | ||||
|      * then it populates the `failedAndPendingContributionList`. | ||||
|      **/ | ||||
|     void getFailedAndPendingContributions() { | ||||
|         final PagedList.Config pagedListConfig = | ||||
|             (new PagedList.Config.Builder()) | ||||
|                 .setPrefetchDistance(50) | ||||
|                 .setPageSize(10).build(); | ||||
|         Factory<Integer, Contribution> factory; | ||||
|         factory = repository.fetchContributionsWithStates( | ||||
|             Arrays.asList(Contribution.STATE_IN_PROGRESS, Contribution.STATE_QUEUED, | ||||
|                 Contribution.STATE_PAUSED, Contribution.STATE_FAILED)); | ||||
| 
 | ||||
|         LivePagedListBuilder livePagedListBuilder = new LivePagedListBuilder(factory, | ||||
|             pagedListConfig); | ||||
|         failedAndPendingContributionList = livePagedListBuilder.build(); | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -13,6 +13,7 @@ import io.reactivex.Completable; | |||
| import io.reactivex.Single; | ||||
| import java.util.Calendar; | ||||
| import java.util.List; | ||||
| import timber.log.Timber; | ||||
| 
 | ||||
| @Dao | ||||
| public abstract class ContributionDao { | ||||
|  | @ -27,6 +28,9 @@ public abstract class ContributionDao { | |||
|         return Completable | ||||
|             .fromAction(() -> { | ||||
|                 contribution.setDateModified(Calendar.getInstance().getTime()); | ||||
|                 if (contribution.getDateUploadStarted() == null) { | ||||
|                     contribution.setDateUploadStarted(Calendar.getInstance().getTime()); | ||||
|                 } | ||||
|                 saveSynchronous(contribution); | ||||
|             }); | ||||
|     } | ||||
|  | @ -44,11 +48,32 @@ public abstract class ContributionDao { | |||
|     @Delete | ||||
|     public abstract void deleteSynchronous(Contribution contribution); | ||||
| 
 | ||||
|     /** | ||||
|      * Deletes contributions with specific states from the database. | ||||
|      * | ||||
|      * @param states The states of the contributions to delete. | ||||
|      * @throws SQLiteException If an SQLite error occurs. | ||||
|      */ | ||||
|     @Query("DELETE FROM contribution WHERE state IN (:states)") | ||||
|     public abstract void deleteContributionsWithStatesSynchronous(List<Integer> states) | ||||
|         throws SQLiteException; | ||||
| 
 | ||||
|     public Completable delete(final Contribution contribution) { | ||||
|         return Completable | ||||
|             .fromAction(() -> deleteSynchronous(contribution)); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Deletes contributions with specific states from the database. | ||||
|      * | ||||
|      * @param states The states of the contributions to delete. | ||||
|      * @return A Completable indicating the result of the operation. | ||||
|      */ | ||||
|     public Completable deleteContributionsWithStates(List<Integer> states) { | ||||
|         return Completable | ||||
|             .fromAction(() -> deleteContributionsWithStatesSynchronous(states)); | ||||
|     } | ||||
| 
 | ||||
|     @Query("SELECT * from contribution WHERE media_filename=:fileName") | ||||
|     public abstract List<Contribution> getContributionWithTitle(String fileName); | ||||
| 
 | ||||
|  | @ -58,6 +83,26 @@ public abstract class ContributionDao { | |||
|     @Query("SELECT * from contribution WHERE state IN (:states) order by media_dateUploaded DESC") | ||||
|     public abstract Single<List<Contribution>> getContribution(List<Integer> states); | ||||
| 
 | ||||
|     /** | ||||
|      * Gets contributions with specific states in descending order by the date they were uploaded. | ||||
|      * | ||||
|      * @param states The states of the contributions to fetch. | ||||
|      * @return A DataSource factory for paginated contributions with the specified states. | ||||
|      */ | ||||
|     @Query("SELECT * from contribution WHERE state IN (:states) order by media_dateUploaded DESC") | ||||
|     public abstract DataSource.Factory<Integer, Contribution> getContributions( | ||||
|         List<Integer> states); | ||||
| 
 | ||||
|     /** | ||||
|      * Gets contributions with specific states in ascending order by the date the upload started. | ||||
|      * | ||||
|      * @param states The states of the contributions to fetch. | ||||
|      * @return A DataSource factory for paginated contributions with the specified states. | ||||
|      */ | ||||
|     @Query("SELECT * from contribution WHERE state IN (:states) order by dateUploadStarted ASC") | ||||
|     public abstract DataSource.Factory<Integer, Contribution> getContributionsSortedByDateUploadStarted( | ||||
|         List<Integer> states); | ||||
| 
 | ||||
|     @Query("SELECT COUNT(*) from contribution WHERE state in (:toUpdateStates)") | ||||
|     public abstract Single<Integer> getPendingUploads(int[] toUpdateStates); | ||||
| 
 | ||||
|  | @ -67,6 +112,15 @@ public abstract class ContributionDao { | |||
|     @Update | ||||
|     public abstract void updateSynchronous(Contribution contribution); | ||||
| 
 | ||||
|     /** | ||||
|      * Updates the state of contributions with specific states. | ||||
|      * | ||||
|      * @param states   The current states of the contributions to update. | ||||
|      * @param newState The new state to set. | ||||
|      */ | ||||
|     @Query("UPDATE contribution SET state = :newState WHERE state IN (:states)") | ||||
|     public abstract void updateContributionsState(List<Integer> states, int newState); | ||||
| 
 | ||||
|     public Completable update(final Contribution contribution) { | ||||
|         return Completable | ||||
|             .fromAction(() -> { | ||||
|  | @ -74,4 +128,18 @@ public abstract class ContributionDao { | |||
|                 updateSynchronous(contribution); | ||||
|             }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Updates the state of contributions with specific states asynchronously. | ||||
|      * | ||||
|      * @param states   The current states of the contributions to update. | ||||
|      * @param newState The new state to set. | ||||
|      * @return A Completable indicating the result of the operation. | ||||
|      */ | ||||
|     public Completable updateContributionsWithStates(List<Integer> states, int newState) { | ||||
|         return Completable | ||||
|             .fromAction(() -> { | ||||
|                 updateContributionsState(states, newState); | ||||
|             }); | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -48,11 +48,8 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder { | |||
| 
 | ||||
|         binding = LayoutContributionBinding.bind(parent); | ||||
| 
 | ||||
|         binding.retryButton.setOnClickListener(v -> retryUpload()); | ||||
|         binding.cancelButton.setOnClickListener(v -> deleteUpload()); | ||||
|         binding.contributionImage.setOnClickListener(v -> imageClicked()); | ||||
|         binding.wikipediaButton.setOnClickListener(v -> wikipediaButtonClicked()); | ||||
|         binding.pauseResumeButton.setOnClickListener(v -> onPauseResumeButtonClicked()); | ||||
| 
 | ||||
|         /* Set a dialog indicating that the upload is being paused. This is needed because pausing | ||||
|         an upload might take a dozen seconds. */ | ||||
|  | @ -80,9 +77,6 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder { | |||
|         binding.contributionImage.getHierarchy().setPlaceholderImage(R.drawable.image_placeholder); | ||||
|         binding.contributionImage.getHierarchy().setFailureImage(R.drawable.image_placeholder); | ||||
| 
 | ||||
|          | ||||
|          | ||||
| 
 | ||||
|         final String imageSource = chooseImageSource(contribution.getMedia().getThumbUrl(), | ||||
|             contribution.getLocalUri()); | ||||
|         if (!TextUtils.isEmpty(imageSource)) { | ||||
|  | @ -90,79 +84,27 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder { | |||
|                 imageRequest = ImageRequestBuilder.newBuilderWithSource(Uri.parse(imageSource)) | ||||
|                     .setProgressiveRenderingEnabled(true) | ||||
|                     .build(); | ||||
|             } | ||||
|             else if (URLUtil.isFileUrl(imageSource)){ | ||||
|                 imageRequest=ImageRequest.fromUri(Uri.parse(imageSource)); | ||||
|             } | ||||
|             else if(imageSource != null) { | ||||
|             } else if (URLUtil.isFileUrl(imageSource)) { | ||||
|                 imageRequest = ImageRequest.fromUri(Uri.parse(imageSource)); | ||||
|             } else if (imageSource != null) { | ||||
|                 final File file = new File(imageSource); | ||||
|                 imageRequest = ImageRequest.fromFile(file); | ||||
|             } | ||||
| 
 | ||||
|             if(imageRequest != null){ | ||||
|             if (imageRequest != null) { | ||||
|                 binding.contributionImage.setImageRequest(imageRequest); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         binding.contributionSequenceNumber.setText(String.valueOf(position + 1)); | ||||
|         binding.contributionSequenceNumber.setVisibility(View.VISIBLE); | ||||
| 
 | ||||
|         binding.wikipediaButton.setVisibility(View.GONE); | ||||
|         switch (contribution.getState()) { | ||||
|             case Contribution.STATE_COMPLETED: | ||||
|         binding.contributionState.setVisibility(View.GONE); | ||||
|         binding.contributionProgress.setVisibility(View.GONE); | ||||
|         binding.imageOptions.setVisibility(View.GONE); | ||||
|         binding.contributionState.setText(""); | ||||
|         checkIfMediaExistsOnWikipediaPage(contribution); | ||||
|                 break; | ||||
|             case Contribution.STATE_QUEUED: | ||||
|             case Contribution.STATE_QUEUED_LIMITED_CONNECTION_MODE: | ||||
|                 binding.contributionProgress.setVisibility(View.GONE); | ||||
|                 binding.contributionState.setVisibility(View.VISIBLE); | ||||
|                 binding.contributionState.setText(R.string.contribution_state_queued); | ||||
|                 binding.imageOptions.setVisibility(View.GONE); | ||||
|                 break; | ||||
|             case Contribution.STATE_IN_PROGRESS: | ||||
|                 binding.contributionState.setVisibility(View.GONE); | ||||
|                 binding.contributionProgress.setVisibility(View.VISIBLE); | ||||
|                 binding.wikipediaButton.setVisibility(View.GONE); | ||||
|                 binding.pauseResumeButton.setVisibility(View.VISIBLE); | ||||
|                 binding.cancelButton.setVisibility(View.GONE); | ||||
|                 binding.retryButton.setVisibility(View.GONE); | ||||
|                 binding.imageOptions.setVisibility(View.VISIBLE); | ||||
|                 final long total = contribution.getDataLength(); | ||||
|                 final long transferred = contribution.getTransferred(); | ||||
|                 if (transferred == 0 || transferred >= total) { | ||||
|                     binding.contributionProgress.setIndeterminate(true); | ||||
|                 } else { | ||||
|                     binding.contributionProgress.setIndeterminate(false); | ||||
|                     binding.contributionProgress.setProgress((int) (((double) transferred / (double) total) * 100)); | ||||
|                 } | ||||
|                 break; | ||||
|             case Contribution.STATE_PAUSED: | ||||
|                 binding.contributionProgress.setVisibility(View.GONE); | ||||
|                 binding.contributionState.setVisibility(View.VISIBLE); | ||||
|                 binding.contributionState.setText(R.string.paused); | ||||
|                 binding.cancelButton.setVisibility(View.VISIBLE); | ||||
|                 binding.retryButton.setVisibility(View.GONE); | ||||
|                 binding.pauseResumeButton.setVisibility(View.VISIBLE); | ||||
|                 binding.imageOptions.setVisibility(View.VISIBLE); | ||||
|                 setResume(); | ||||
|                 if(pausingPopUp.isShowing()){ | ||||
|                     pausingPopUp.hide(); | ||||
|                 } | ||||
|                 break; | ||||
|             case Contribution.STATE_FAILED: | ||||
|                 binding.contributionState.setVisibility(View.VISIBLE); | ||||
|                 binding.contributionState.setText(R.string.contribution_state_failed); | ||||
|                 binding.contributionProgress.setVisibility(View.GONE); | ||||
|                 binding.cancelButton.setVisibility(View.VISIBLE); | ||||
|                 binding.retryButton.setVisibility(View.VISIBLE); | ||||
|                 binding.pauseResumeButton.setVisibility(View.GONE); | ||||
|                 binding.imageOptions.setVisibility(View.VISIBLE); | ||||
|                 break; | ||||
|         } | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | @ -196,8 +138,6 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder { | |||
|         if (!mediaExists) { | ||||
|             binding.wikipediaButton.setVisibility(View.VISIBLE); | ||||
|             isWikipediaButtonDisplayed = true; | ||||
|             binding.cancelButton.setVisibility(View.GONE); | ||||
|             binding.retryButton.setVisibility(View.GONE); | ||||
|             binding.imageOptions.setVisibility(View.VISIBLE); | ||||
|         } | ||||
|     } | ||||
|  | @ -217,20 +157,6 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder { | |||
|                 null; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Retry upload when it is failed | ||||
|      */ | ||||
|     public void retryUpload() { | ||||
|         callback.retryUpload(contribution); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Delete a failed upload attempt | ||||
|      */ | ||||
|     public void deleteUpload() { | ||||
|         callback.deleteUpload(contribution); | ||||
|     } | ||||
| 
 | ||||
|     public void imageClicked() { | ||||
|         callback.openMediaDetail(position, isWikipediaButtonDisplayed); | ||||
|     } | ||||
|  | @ -239,44 +165,6 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder { | |||
|         callback.addImageToWikipedia(contribution); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Triggers a callback for pause/resume | ||||
|      */ | ||||
|     public void onPauseResumeButtonClicked() { | ||||
|         if (binding.pauseResumeButton.getTag().toString().equals("pause")) { | ||||
|             pause(); | ||||
|         } else { | ||||
|             resume(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private void resume() { | ||||
|         callback.resumeUpload(contribution); | ||||
|         setPaused(); | ||||
|     } | ||||
| 
 | ||||
|     private void pause() { | ||||
|         pausingPopUp.show(); | ||||
|         callback.pauseUpload(contribution); | ||||
|         setResume(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Update pause/resume button to show pause state | ||||
|      */ | ||||
|     private void setPaused() { | ||||
|         binding.pauseResumeButton.setImageResource(R.drawable.pause_icon); | ||||
|         binding.pauseResumeButton.setTag(parent.getContext().getString(R.string.pause)); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Update pause/resume button to show resume state | ||||
|      */ | ||||
|     private void setResume() { | ||||
|         binding.pauseResumeButton.setImageResource(R.drawable.play_icon); | ||||
|         binding.pauseResumeButton.setTag(parent.getContext().getString(R.string.resume)); | ||||
|     } | ||||
| 
 | ||||
|     public ImageRequest getImageRequest() { | ||||
|         return imageRequest; | ||||
|     } | ||||
|  |  | |||
|  | @ -19,8 +19,5 @@ public class ContributionsContract { | |||
| 
 | ||||
|         Contribution getContributionsWithTitle(String uri); | ||||
| 
 | ||||
|         void deleteUpload(Contribution contribution); | ||||
| 
 | ||||
|         void saveContribution(Contribution contribution); | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -5,6 +5,7 @@ import static fr.free.nrw.commons.contributions.Contribution.STATE_FAILED; | |||
| import static fr.free.nrw.commons.contributions.Contribution.STATE_PAUSED; | ||||
| import static fr.free.nrw.commons.nearby.fragments.NearbyParentFragment.WLM_URL; | ||||
| import static fr.free.nrw.commons.profile.ProfileActivity.KEY_USERNAME; | ||||
| import static fr.free.nrw.commons.utils.ImageUtils.IMAGE_OK; | ||||
| import static fr.free.nrw.commons.utils.LengthUtils.computeBearing; | ||||
| import static fr.free.nrw.commons.utils.LengthUtils.formatDistanceBetween; | ||||
| 
 | ||||
|  | @ -12,6 +13,7 @@ import android.Manifest; | |||
| import android.Manifest.permission; | ||||
| import android.annotation.SuppressLint; | ||||
| import android.content.Context; | ||||
| import android.content.Intent; | ||||
| import android.hardware.Sensor; | ||||
| import android.hardware.SensorEvent; | ||||
| import android.hardware.SensorEventListener; | ||||
|  | @ -25,6 +27,7 @@ import android.view.MenuItem.OnMenuItemClickListener; | |||
| import android.view.View; | ||||
| import android.view.ViewGroup; | ||||
| import android.widget.CheckBox; | ||||
| import android.widget.ImageView; | ||||
| import android.widget.LinearLayout; | ||||
| import android.widget.TextView; | ||||
| import android.widget.Toast; | ||||
|  | @ -44,6 +47,8 @@ import fr.free.nrw.commons.notification.models.Notification; | |||
| import fr.free.nrw.commons.notification.NotificationController; | ||||
| import fr.free.nrw.commons.profile.ProfileActivity; | ||||
| import fr.free.nrw.commons.theme.BaseActivity; | ||||
| import fr.free.nrw.commons.upload.UploadProgressActivity; | ||||
| import java.util.Calendar; | ||||
| import java.util.Date; | ||||
| import java.util.List; | ||||
| import java.util.Map; | ||||
|  | @ -104,6 +109,8 @@ public class ContributionsFragment | |||
|     LocationServiceManager locationManager; | ||||
|     @Inject | ||||
|     NotificationController notificationController; | ||||
|     @Inject | ||||
|     ContributionController contributionController; | ||||
| 
 | ||||
|     private CompositeDisposable compositeDisposable = new CompositeDisposable(); | ||||
| 
 | ||||
|  | @ -113,10 +120,10 @@ public class ContributionsFragment | |||
|     static final String MEDIA_DETAIL_PAGER_FRAGMENT_TAG = "MediaDetailFragmentTag"; | ||||
|     private static final int MAX_RETRIES = 10; | ||||
| 
 | ||||
| 
 | ||||
|     public FragmentContributionsBinding binding; | ||||
| 
 | ||||
|     @Inject ContributionsPresenter contributionsPresenter; | ||||
|     @Inject | ||||
|     ContributionsPresenter contributionsPresenter; | ||||
| 
 | ||||
|     @Inject | ||||
|     SessionManager sessionManager; | ||||
|  | @ -129,6 +136,12 @@ public class ContributionsFragment | |||
| 
 | ||||
|     public TextView notificationCount; | ||||
| 
 | ||||
|     public TextView pendingUploadsCountTextView; | ||||
| 
 | ||||
|     public TextView uploadsErrorTextView; | ||||
| 
 | ||||
|     public ImageView pendingUploadsImageView; | ||||
| 
 | ||||
|     private Campaign wlmCampaign; | ||||
| 
 | ||||
|     String userName; | ||||
|  | @ -150,10 +163,12 @@ public class ContributionsFragment | |||
|                 if (areAllGranted) { | ||||
|                     onLocationPermissionGranted(); | ||||
|                 } else { | ||||
|                 if (shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_FINE_LOCATION) | ||||
|                     if (shouldShowRequestPermissionRationale( | ||||
|                         Manifest.permission.ACCESS_FINE_LOCATION) | ||||
|                         && store.getBoolean("displayLocationPermissionForCardView", true) | ||||
|                         && !store.getBoolean("doNotAskForLocationPermission", false) | ||||
|                     && (((MainActivity) getActivity()).activeFragment == ActiveFragment.CONTRIBUTIONS)) { | ||||
|                         && (((MainActivity) getActivity()).activeFragment | ||||
|                         == ActiveFragment.CONTRIBUTIONS)) { | ||||
|                         binding.cardViewNearby.permissionType = NearbyNotificationCardView.PermissionType.ENABLE_LOCATION_PERMISSION; | ||||
|                     } else { | ||||
|                         displayYouWontSeeNearbyMessage(); | ||||
|  | @ -198,11 +213,10 @@ public class ContributionsFragment | |||
|         checkBox.setOnCheckedChangeListener((buttonView, isChecked) -> { | ||||
|             if (isChecked) { | ||||
|                 // Do not ask for permission on activity start again | ||||
|                 store.putBoolean("displayLocationPermissionForCardView",false); | ||||
|                 store.putBoolean("displayLocationPermissionForCardView", false); | ||||
|             } | ||||
|         }); | ||||
| 
 | ||||
| 
 | ||||
|         if (savedInstanceState != null) { | ||||
|             mediaDetailPagerFragment = (MediaDetailPagerFragment) getChildFragmentManager() | ||||
|                 .findFragmentByTag(MEDIA_DETAIL_PAGER_FRAGMENT_TAG); | ||||
|  | @ -212,9 +226,7 @@ public class ContributionsFragment | |||
|         } | ||||
| 
 | ||||
|         initFragments(); | ||||
|         if(isUserProfile) { | ||||
|             binding.limitedConnectionEnabledLayout.setVisibility(View.GONE); | ||||
|         }else { | ||||
|         if (!isUserProfile) { | ||||
|             upDateUploadCount(); | ||||
|         } | ||||
|         if (shouldShowMediaDetailsFragment) { | ||||
|  | @ -230,7 +242,6 @@ public class ContributionsFragment | |||
|             && sessionManager.getCurrentAccount() != null && !isUserProfile) { | ||||
|             setUploadCount(); | ||||
|         } | ||||
|         binding.limitedConnectionEnabledLayout.setOnClickListener(toggleDescriptionListener); | ||||
|         setHasOptionsMenu(true); | ||||
|         return binding.getRoot(); | ||||
|     } | ||||
|  | @ -258,10 +269,32 @@ public class ContributionsFragment | |||
|         MenuItem notificationsMenuItem = menu.findItem(R.id.notifications); | ||||
|         final View notification = notificationsMenuItem.getActionView(); | ||||
|         notificationCount = notification.findViewById(R.id.notification_count_badge); | ||||
|         MenuItem uploadMenuItem = menu.findItem(R.id.upload_tab); | ||||
|         final View uploadMenuItemActionView = uploadMenuItem.getActionView(); | ||||
|         pendingUploadsCountTextView = uploadMenuItemActionView.findViewById( | ||||
|             R.id.pending_uploads_count_badge); | ||||
|         uploadsErrorTextView = uploadMenuItemActionView.findViewById( | ||||
|             R.id.uploads_error_count_badge); | ||||
|         pendingUploadsImageView = uploadMenuItemActionView.findViewById( | ||||
|             R.id.pending_uploads_image_view); | ||||
|         if (pendingUploadsImageView != null) { | ||||
|             pendingUploadsImageView.setOnClickListener(view -> { | ||||
|                 startActivity(new Intent(getContext(), UploadProgressActivity.class)); | ||||
|             }); | ||||
|         } | ||||
|         if (pendingUploadsCountTextView != null) { | ||||
|             pendingUploadsCountTextView.setOnClickListener(view -> { | ||||
|                 startActivity(new Intent(getContext(), UploadProgressActivity.class)); | ||||
|             }); | ||||
|         } | ||||
|         if (uploadsErrorTextView != null) { | ||||
|             uploadsErrorTextView.setOnClickListener(view -> { | ||||
|                 startActivity(new Intent(getContext(), UploadProgressActivity.class)); | ||||
|             }); | ||||
|         } | ||||
|         notification.setOnClickListener(view -> { | ||||
|             NotificationActivity.startYourself(getContext(), "unread"); | ||||
|         }); | ||||
|         updateLimitedConnectionToggle(menu); | ||||
|     } | ||||
| 
 | ||||
|     @SuppressLint("CheckResult") | ||||
|  | @ -273,6 +306,33 @@ public class ContributionsFragment | |||
|                 throwable -> Timber.e(throwable, "Error occurred while loading notifications"))); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Sets the visibility of the upload icon based on the number of failed and pending | ||||
|      * contributions. | ||||
|      */ | ||||
|     public void setUploadIconVisibility() { | ||||
|         contributionController.getFailedAndPendingContributions(); | ||||
|         contributionController.failedAndPendingContributionList.observe(getViewLifecycleOwner(), | ||||
|             list -> { | ||||
|                 updateUploadIcon(list.size()); | ||||
|             }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Sets the count for the upload icon based on the number of pending and failed contributions. | ||||
|      */ | ||||
|     public void setUploadIconCount() { | ||||
|         contributionController.getPendingContributions(); | ||||
|         contributionController.pendingContributionList.observe(getViewLifecycleOwner(), | ||||
|             list -> { | ||||
|                 updatePendingIcon(list.size()); | ||||
|             }); | ||||
|         contributionController.getFailedContributions(); | ||||
|         contributionController.failedContributionList.observe(getViewLifecycleOwner(), list -> { | ||||
|             updateErrorIcon(list.size()); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     public void scrollToTop() { | ||||
|         if (contributionsListFragment != null) { | ||||
|             contributionsListFragment.scrollToTop(); | ||||
|  | @ -289,29 +349,6 @@ public class ContributionsFragment | |||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public void updateLimitedConnectionToggle(Menu menu) { | ||||
|         MenuItem checkable = menu.findItem(R.id.toggle_limited_connection_mode); | ||||
|         boolean isEnabled = store | ||||
|             .getBoolean(CommonsApplication.IS_LIMITED_CONNECTION_MODE_ENABLED, false); | ||||
| 
 | ||||
|         checkable.setChecked(isEnabled); | ||||
|         if (binding!=null) { | ||||
|             binding.limitedConnectionEnabledLayout.setVisibility(isEnabled ? View.VISIBLE : View.GONE); | ||||
|         } | ||||
| 
 | ||||
|         checkable.setIcon((isEnabled) ? R.drawable.ic_baseline_cloud_off_24:R.drawable.ic_baseline_cloud_queue_24); | ||||
|         checkable.setOnMenuItemClickListener(new OnMenuItemClickListener() { | ||||
|             @Override | ||||
|             public boolean onMenuItemClick(MenuItem item) { | ||||
|                 ((MainActivity) getActivity()).toggleLimitedConnectionMode(); | ||||
|                 boolean isEnabled = store.getBoolean(CommonsApplication.IS_LIMITED_CONNECTION_MODE_ENABLED, false); | ||||
|                 binding.limitedConnectionEnabledLayout.setVisibility(isEnabled ? View.VISIBLE : View.GONE); | ||||
|                 checkable.setIcon((isEnabled) ? R.drawable.ic_baseline_cloud_off_24:R.drawable.ic_baseline_cloud_queue_24); | ||||
|                 return false; | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onAttach(Context context) { | ||||
|         super.onAttach(context); | ||||
|  | @ -355,7 +392,7 @@ public class ContributionsFragment | |||
|     } | ||||
| 
 | ||||
|     private void setupViewForMediaDetails() { | ||||
|         if (binding!=null) { | ||||
|         if (binding != null) { | ||||
|             binding.campaignsView.setVisibility(View.GONE); | ||||
|         } | ||||
|     } | ||||
|  | @ -465,7 +502,7 @@ public class ContributionsFragment | |||
|         contributionsPresenter.onAttachView(this); | ||||
|         locationManager.addLocationListener(this); | ||||
| 
 | ||||
|         if (binding==null) { | ||||
|         if (binding == null) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|  | @ -484,7 +521,8 @@ public class ContributionsFragment | |||
|                 } catch (Exception e) { | ||||
|                     Timber.e(e); | ||||
|                 } | ||||
|                 if (binding.cardViewNearby.cardViewVisibilityState == NearbyNotificationCardView.CardViewVisibilityState.READY) { | ||||
|                 if (binding.cardViewNearby.cardViewVisibilityState | ||||
|                     == NearbyNotificationCardView.CardViewVisibilityState.READY) { | ||||
|                     binding.cardViewNearby.setVisibility(View.VISIBLE); | ||||
|                 } | ||||
| 
 | ||||
|  | @ -494,16 +532,19 @@ public class ContributionsFragment | |||
|             } | ||||
| 
 | ||||
|             // Notification Count and Campaigns should not be set, if it is used in User Profile | ||||
|             if(!isUserProfile) { | ||||
|             if (!isUserProfile) { | ||||
|                 setNotificationCount(); | ||||
|                 fetchCampaigns(); | ||||
|                 setUploadIconVisibility(); | ||||
|                 setUploadIconCount(); | ||||
|             } | ||||
|         } | ||||
|         mSensorManager.registerListener(this, mLight, SensorManager.SENSOR_DELAY_UI); | ||||
|     } | ||||
| 
 | ||||
|     private void checkPermissionsAndShowNearbyCardView() { | ||||
|         if (PermissionUtils.hasPermission(getActivity(), new String[]{Manifest.permission.ACCESS_FINE_LOCATION})) { | ||||
|         if (PermissionUtils.hasPermission(getActivity(), | ||||
|             new String[]{Manifest.permission.ACCESS_FINE_LOCATION})) { | ||||
|             onLocationPermissionGranted(); | ||||
|         } else if (shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_FINE_LOCATION) | ||||
|             && store.getBoolean("displayLocationPermissionForCardView", true) | ||||
|  | @ -636,14 +677,14 @@ public class ContributionsFragment | |||
|      */ | ||||
|     private void fetchCampaigns() { | ||||
|         if (Utils.isMonumentsEnabled(new Date())) { | ||||
|             if (binding!=null) { | ||||
|             if (binding != null) { | ||||
|                 binding.campaignsView.setCampaign(wlmCampaign); | ||||
|                 binding.campaignsView.setVisibility(View.VISIBLE); | ||||
|             } | ||||
|         } else if (store.getBoolean(CampaignView.CAMPAIGNS_DEFAULT_PREFERENCE, true)) { | ||||
|             presenter.getCampaigns(); | ||||
|         } else { | ||||
|             if (binding!=null) { | ||||
|             if (binding != null) { | ||||
|                 binding.campaignsView.setVisibility(View.GONE); | ||||
|             } | ||||
|         } | ||||
|  | @ -657,7 +698,7 @@ public class ContributionsFragment | |||
|     @Override | ||||
|     public void showCampaigns(Campaign campaign) { | ||||
|         if (campaign != null && !isUserProfile) { | ||||
|             if (binding!=null) { | ||||
|             if (binding != null) { | ||||
|                 binding.campaignsView.setCampaign(campaign); | ||||
|             } | ||||
|         } | ||||
|  | @ -676,67 +717,6 @@ public class ContributionsFragment | |||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Restarts the upload process for a contribution | ||||
|      * | ||||
|      * @param contribution | ||||
|      */ | ||||
|     public void restartUpload(Contribution contribution) { | ||||
|         contribution.setState(Contribution.STATE_QUEUED); | ||||
|         contributionsPresenter.saveContribution(contribution); | ||||
|         Timber.d("Restarting for %s", contribution.toString()); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Retry upload when it is failed | ||||
|      * | ||||
|      * @param contribution contribution to be retried | ||||
|      */ | ||||
|     @Override | ||||
|     public void retryUpload(Contribution contribution) { | ||||
|         if (NetworkUtils.isInternetConnectionEstablished(getContext())) { | ||||
|             if (contribution.getState() == STATE_PAUSED | ||||
|                 || contribution.getState() == Contribution.STATE_QUEUED_LIMITED_CONNECTION_MODE) { | ||||
|                 restartUpload(contribution); | ||||
|             } else if (contribution.getState() == STATE_FAILED) { | ||||
|                 int retries = contribution.getRetries(); | ||||
|                 // TODO: Improve UX. Additional details: https://github.com/commons-app/apps-android-commons/pull/5257#discussion_r1304662562 | ||||
|                 /* Limit the number of retries for a failed upload | ||||
|                    to handle cases like invalid filename as such uploads | ||||
|                    will never be successful */ | ||||
|                 if (retries < MAX_RETRIES) { | ||||
|                     contribution.setRetries(retries + 1); | ||||
|                     Timber.d("Retried uploading %s %d times", contribution.getMedia().getFilename(), | ||||
|                         retries + 1); | ||||
|                     restartUpload(contribution); | ||||
|                 } else { | ||||
|                     // TODO: Show the exact reason for failure | ||||
|                     Toast.makeText(getContext(), | ||||
|                         R.string.retry_limit_reached, Toast.LENGTH_SHORT).show(); | ||||
|                 } | ||||
|             } else { | ||||
|                 Timber.d("Skipping re-upload for non-failed %s", contribution.toString()); | ||||
|             } | ||||
|         } else { | ||||
|             ViewUtil.showLongToast(getContext(), R.string.this_function_needs_network_connection); | ||||
|         } | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Pauses the upload | ||||
|      * | ||||
|      * @param contribution | ||||
|      */ | ||||
|     @Override | ||||
|     public void pauseUpload(Contribution contribution) { | ||||
|         //Pause the upload in the global singleton | ||||
|         CommonsApplication.pauseUploads.put(contribution.getPageId(), true); | ||||
|         //Retain the paused state in DB | ||||
|         contribution.setState(STATE_PAUSED); | ||||
|         contributionsPresenter.saveContribution(contribution); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Notify the viewpager that number of items have changed. | ||||
|      */ | ||||
|  | @ -747,6 +727,54 @@ public class ContributionsFragment | |||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Updates the visibility and text of the pending uploads count TextView based on the given | ||||
|      * count. | ||||
|      * | ||||
|      * @param pendingCount The number of pending uploads. | ||||
|      */ | ||||
|     public void updatePendingIcon(int pendingCount) { | ||||
|         if (pendingUploadsCountTextView != null) { | ||||
|             if (pendingCount != 0) { | ||||
|                 pendingUploadsCountTextView.setVisibility(View.VISIBLE); | ||||
|                 pendingUploadsCountTextView.setText(String.valueOf(pendingCount)); | ||||
|             } else { | ||||
|                 pendingUploadsCountTextView.setVisibility(View.INVISIBLE); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Updates the visibility and text of the error uploads TextView based on the given count. | ||||
|      * | ||||
|      * @param errorCount The number of error uploads. | ||||
|      */ | ||||
|     public void updateErrorIcon(int errorCount) { | ||||
|         if (uploadsErrorTextView != null) { | ||||
|             if (errorCount != 0) { | ||||
|                 uploadsErrorTextView.setVisibility(View.VISIBLE); | ||||
|                 uploadsErrorTextView.setText(String.valueOf(errorCount)); | ||||
|             } else { | ||||
|                 uploadsErrorTextView.setVisibility(View.GONE); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Updates the visibility of the pending uploads ImageView based on the given count. | ||||
|      * | ||||
|      * @param count The number of pending uploads. | ||||
|      */ | ||||
|     public void updateUploadIcon(int count) { | ||||
|         if (pendingUploadsImageView != null) { | ||||
|             if (count != 0) { | ||||
|                 pendingUploadsImageView.setVisibility(View.VISIBLE); | ||||
|             } else { | ||||
|                 pendingUploadsImageView.setVisibility(View.GONE); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Replace whatever is in the current contributionsFragmentContainer view with | ||||
|      * mediaDetailPagerFragment, and preserve previous state in back stack. Called when user selects | ||||
|  | @ -782,7 +810,8 @@ public class ContributionsFragment | |||
|     public boolean backButtonClicked() { | ||||
|         if (mediaDetailPagerFragment != null && mediaDetailPagerFragment.isVisible()) { | ||||
|             if (store.getBoolean("displayNearbyCardView", true) && !isUserProfile) { | ||||
|                 if (binding.cardViewNearby.cardViewVisibilityState == NearbyNotificationCardView.CardViewVisibilityState.READY) { | ||||
|                 if (binding.cardViewNearby.cardViewVisibilityState | ||||
|                     == NearbyNotificationCardView.CardViewVisibilityState.READY) { | ||||
|                     binding.cardViewNearby.setVisibility(View.VISIBLE); | ||||
|                 } | ||||
|             } else { | ||||
|  | @ -829,6 +858,60 @@ public class ContributionsFragment | |||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     /** | ||||
|      * Restarts the upload process for a contribution | ||||
|      * | ||||
|      * @param contribution | ||||
|      */ | ||||
|     public void restartUpload(Contribution contribution) { | ||||
|         contribution.setDateUploadStarted(Calendar.getInstance().getTime()); | ||||
|         if (contribution.getState() == Contribution.STATE_FAILED) { | ||||
|             if (contribution.getErrorInfo() == null) { | ||||
|                 contribution.setChunkInfo(null); | ||||
|                 contribution.setTransferred(0); | ||||
|             } | ||||
|             contributionsPresenter.checkDuplicateImageAndRestartContribution(contribution); | ||||
|         } else { | ||||
|             contribution.setState(Contribution.STATE_QUEUED); | ||||
|             contributionsPresenter.saveContribution(contribution); | ||||
|             Timber.d("Restarting for %s", contribution.toString()); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Retry upload when it is failed | ||||
|      * | ||||
|      * @param contribution contribution to be retried | ||||
|      */ | ||||
|     public void retryUpload(Contribution contribution) { | ||||
|         if (NetworkUtils.isInternetConnectionEstablished(getContext())) { | ||||
|             if (contribution.getState() == STATE_PAUSED) { | ||||
|                 restartUpload(contribution); | ||||
|             } else if (contribution.getState() == STATE_FAILED) { | ||||
|                 int retries = contribution.getRetries(); | ||||
|                 // TODO: Improve UX. Additional details: https://github.com/commons-app/apps-android-commons/pull/5257#discussion_r1304662562 | ||||
|                 /* Limit the number of retries for a failed upload | ||||
|                    to handle cases like invalid filename as such uploads | ||||
|                    will never be successful */ | ||||
|                 if (retries < MAX_RETRIES) { | ||||
|                     contribution.setRetries(retries + 1); | ||||
|                     Timber.d("Retried uploading %s %d times", contribution.getMedia().getFilename(), | ||||
|                         retries + 1); | ||||
|                     restartUpload(contribution); | ||||
|                 } else { | ||||
|                     // TODO: Show the exact reason for failure | ||||
|                     Toast.makeText(getContext(), | ||||
|                         R.string.retry_limit_reached, Toast.LENGTH_SHORT).show(); | ||||
|                 } | ||||
|             } else { | ||||
|                 Timber.d("Skipping re-upload for non-failed %s", contribution.toString()); | ||||
|             } | ||||
|         } else { | ||||
|             ViewUtil.showLongToast(getContext(), R.string.this_function_needs_network_connection); | ||||
|         } | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Reload media detail fragment once media is nominated | ||||
|      * | ||||
|  | @ -844,21 +927,6 @@ public class ContributionsFragment | |||
|         } | ||||
|     } | ||||
| 
 | ||||
|     // click listener to toggle description that means uses can press the limited connection | ||||
|     // banner and description will hide. Tap again to show description. | ||||
|     private View.OnClickListener toggleDescriptionListener = new View.OnClickListener() { | ||||
| 
 | ||||
|         @Override | ||||
|         public void onClick(View view) { | ||||
|             View view2 = binding.limitedConnectionDescriptionTextView; | ||||
|             if (view2.getVisibility() == View.GONE) { | ||||
|                 view2.setVisibility(View.VISIBLE); | ||||
|             } else { | ||||
|                 view2.setVisibility(View.GONE); | ||||
|             } | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     /** | ||||
|      * When the device rotates, rotate the Nearby banner's compass arrow in tandem. | ||||
|      */ | ||||
|  |  | |||
|  | @ -70,16 +70,8 @@ public class ContributionsListAdapter extends | |||
| 
 | ||||
|     public interface Callback { | ||||
| 
 | ||||
|         void retryUpload(Contribution contribution); | ||||
| 
 | ||||
|         void deleteUpload(Contribution contribution); | ||||
| 
 | ||||
|         void openMediaDetail(int contribution, boolean isWikipediaPageExists); | ||||
| 
 | ||||
|         void addImageToWikipedia(Contribution contribution); | ||||
| 
 | ||||
|         void pauseUpload(Contribution contribution); | ||||
| 
 | ||||
|         void resumeUpload(Contribution contribution); | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -17,7 +17,5 @@ public class ContributionsListContract { | |||
|     } | ||||
| 
 | ||||
|     public interface UserActionListener extends BasePresenter<View> { | ||||
| 
 | ||||
|         void deleteUpload(Contribution contribution); | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -19,7 +19,7 @@ import android.view.animation.AnimationUtils; | |||
| import android.widget.LinearLayout; | ||||
| import androidx.activity.result.ActivityResultCallback; | ||||
| import androidx.activity.result.ActivityResultLauncher; | ||||
| import androidx.activity.result.contract.ActivityResultContracts; | ||||
| import androidx.activity.result.contract.ActivityResultContracts.RequestMultiplePermissions; | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.annotation.Nullable; | ||||
| import androidx.annotation.VisibleForTesting; | ||||
|  | @ -30,11 +30,11 @@ import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver; | |||
| import androidx.recyclerview.widget.RecyclerView.ItemAnimator; | ||||
| import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener; | ||||
| import androidx.recyclerview.widget.SimpleItemAnimator; | ||||
| import fr.free.nrw.commons.CommonsApplication; | ||||
| import fr.free.nrw.commons.Media; | ||||
| import fr.free.nrw.commons.R; | ||||
| import fr.free.nrw.commons.Utils; | ||||
| import fr.free.nrw.commons.auth.SessionManager; | ||||
| import fr.free.nrw.commons.contributions.ContributionsListAdapter.Callback; | ||||
| import fr.free.nrw.commons.databinding.FragmentContributionsListBinding; | ||||
| import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; | ||||
| import fr.free.nrw.commons.media.MediaClient; | ||||
|  | @ -42,7 +42,6 @@ import fr.free.nrw.commons.profile.ProfileActivity; | |||
| import fr.free.nrw.commons.utils.DialogUtil; | ||||
| import fr.free.nrw.commons.utils.SystemThemeUtils; | ||||
| import fr.free.nrw.commons.utils.ViewUtil; | ||||
| import java.util.Locale; | ||||
| import java.util.Map; | ||||
| import java.util.Objects; | ||||
| import javax.inject.Inject; | ||||
|  | @ -56,7 +55,7 @@ import fr.free.nrw.commons.wikidata.model.WikiSite; | |||
|  */ | ||||
| 
 | ||||
| public class ContributionsListFragment extends CommonsDaggerSupportFragment implements | ||||
|     ContributionsListContract.View, ContributionsListAdapter.Callback, | ||||
|     ContributionsListContract.View, Callback, | ||||
|     WikipediaInstructionsDialogFragment.Callback { | ||||
| 
 | ||||
|     private static final String RV_STATE = "rv_scroll_state"; | ||||
|  | @ -81,7 +80,6 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl | |||
|     private Animation rotate_forward; | ||||
|     private Animation rotate_backward; | ||||
|     private boolean isFabOpen; | ||||
| 
 | ||||
|     @VisibleForTesting | ||||
|     protected RecyclerView rvContributionsList; | ||||
| 
 | ||||
|  | @ -99,7 +97,7 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl | |||
|     private String userName; | ||||
| 
 | ||||
|     private ActivityResultLauncher<String[]> inAppCameraLocationPermissionLauncher = registerForActivityResult( | ||||
|         new ActivityResultContracts.RequestMultiplePermissions(), | ||||
|         new RequestMultiplePermissions(), | ||||
|         new ActivityResultCallback<Map<String, Boolean>>() { | ||||
|             @Override | ||||
|             public void onActivityResult(Map<String, Boolean> result) { | ||||
|  | @ -151,7 +149,7 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl | |||
|         contributionsListPresenter.onAttachView(this); | ||||
|         binding.fabCustomGallery.setOnClickListener(v -> launchCustomSelector()); | ||||
|         binding.fabCustomGallery.setOnLongClickListener(view -> { | ||||
|             ViewUtil.showShortToast(getContext(),R.string.custom_selector_title); | ||||
|             ViewUtil.showShortToast(getContext(), R.string.custom_selector_title); | ||||
|             return true; | ||||
|         }); | ||||
| 
 | ||||
|  | @ -160,7 +158,8 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl | |||
|             binding.fabLayout.setVisibility(VISIBLE); | ||||
|         } else { | ||||
|             binding.tvContributionsOfUser.setVisibility(VISIBLE); | ||||
|             binding.tvContributionsOfUser.setText(getString(R.string.contributions_of_user, userName)); | ||||
|             binding.tvContributionsOfUser.setText( | ||||
|                 getString(R.string.contributions_of_user, userName)); | ||||
|             binding.fabLayout.setVisibility(GONE); | ||||
|         } | ||||
| 
 | ||||
|  | @ -305,7 +304,8 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl | |||
|     public void onConfigurationChanged(final Configuration newConfig) { | ||||
|         super.onConfigurationChanged(newConfig); | ||||
|         // check orientation | ||||
|         binding.fabLayout.setOrientation(newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE ? | ||||
|         binding.fabLayout.setOrientation( | ||||
|             newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE ? | ||||
|                 LinearLayout.HORIZONTAL : LinearLayout.VERTICAL); | ||||
|         rvContributionsList | ||||
|             .setLayoutManager( | ||||
|  | @ -326,7 +326,7 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl | |||
|             animateFAB(isFabOpen); | ||||
|         }); | ||||
|         binding.fabCamera.setOnLongClickListener(view -> { | ||||
|             ViewUtil.showShortToast(getContext(),R.string.add_contribution_from_camera); | ||||
|             ViewUtil.showShortToast(getContext(), R.string.add_contribution_from_camera); | ||||
|             return true; | ||||
|         }); | ||||
|         binding.fabGallery.setOnClickListener(view -> { | ||||
|  | @ -334,7 +334,7 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl | |||
|             animateFAB(isFabOpen); | ||||
|         }); | ||||
|         binding.fabGallery.setOnLongClickListener(view -> { | ||||
|             ViewUtil.showShortToast(getContext(),R.string.menu_from_gallery); | ||||
|             ViewUtil.showShortToast(getContext(), R.string.menu_from_gallery); | ||||
|             return true; | ||||
|         }); | ||||
|     } | ||||
|  | @ -415,30 +415,6 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl | |||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void retryUpload(final Contribution contribution) { | ||||
|         if (null != callback) {//Just being safe, ideally they won't be called when detached | ||||
|             callback.retryUpload(contribution); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void deleteUpload(final Contribution contribution) { | ||||
|         DialogUtil.showAlertDialog(getActivity(), | ||||
|             String.format(Locale.getDefault(), | ||||
|                 getString(R.string.cancelling_upload)), | ||||
|             String.format(Locale.getDefault(), | ||||
|                 getString(R.string.cancel_upload_dialog)), | ||||
|             String.format(Locale.getDefault(), getString(R.string.yes)), String.format(Locale.getDefault(), getString(R.string.no)), | ||||
|             () -> { | ||||
|                 ViewUtil.showShortToast(getContext(), R.string.cancelling_upload); | ||||
|                 contributionsListPresenter.deleteUpload(contribution); | ||||
|                 CommonsApplication.cancelledUploads.add(contribution.getPageId()); | ||||
|             }, () -> { | ||||
|                 // Do nothing | ||||
|             }); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void openMediaDetail(final int position, boolean isWikipediaButtonDisplayed) { | ||||
|         if (null != callback) {//Just being safe, ideally they won't be called when detached | ||||
|  | @ -463,28 +439,6 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl | |||
|             }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Pauses the current upload | ||||
|      * | ||||
|      * @param contribution | ||||
|      */ | ||||
|     @Override | ||||
|     public void pauseUpload(Contribution contribution) { | ||||
|         ViewUtil.showShortToast(getContext(), R.string.pausing_upload); | ||||
|         callback.pauseUpload(contribution); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Resumes the current upload | ||||
|      * | ||||
|      * @param contribution | ||||
|      */ | ||||
|     @Override | ||||
|     public void resumeUpload(Contribution contribution) { | ||||
|         ViewUtil.showShortToast(getContext(), R.string.resuming_upload); | ||||
|         callback.retryUpload(contribution); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Display confirmation dialog with instructions when the user tries to add image to wikipedia | ||||
|      * | ||||
|  | @ -536,13 +490,10 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl | |||
| 
 | ||||
|         void notifyDataSetChanged(); | ||||
| 
 | ||||
|         void retryUpload(Contribution contribution); | ||||
| 
 | ||||
|         void showDetail(int position, boolean isWikipediaButtonDisplayed); | ||||
| 
 | ||||
|         void pauseUpload(Contribution contribution); | ||||
| 
 | ||||
|         // Notify the viewpager that number of items have changed. | ||||
|         void viewPagerNotifyDataSetChanged(); | ||||
| 
 | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -10,6 +10,8 @@ import fr.free.nrw.commons.contributions.ContributionsListContract.UserActionLis | |||
| import fr.free.nrw.commons.di.CommonsApplicationModule; | ||||
| import io.reactivex.Scheduler; | ||||
| import io.reactivex.disposables.CompositeDisposable; | ||||
| import java.util.Arrays; | ||||
| import java.util.Collections; | ||||
| import javax.inject.Inject; | ||||
| import javax.inject.Named; | ||||
| 
 | ||||
|  | @ -36,7 +38,7 @@ public class ContributionsListPresenter implements UserActionListener { | |||
|         this.contributionBoundaryCallback = contributionBoundaryCallback; | ||||
|         this.repository = repository; | ||||
|         this.ioThreadScheduler = ioThreadScheduler; | ||||
|         this.contributionsRemoteDataSource=contributionsRemoteDataSource; | ||||
|         this.contributionsRemoteDataSource = contributionsRemoteDataSource; | ||||
|         compositeDisposable = new CompositeDisposable(); | ||||
|     } | ||||
| 
 | ||||
|  | @ -71,10 +73,12 @@ public class ContributionsListPresenter implements UserActionListener { | |||
|         } else { | ||||
|             contributionBoundaryCallback.setUserName(userName); | ||||
|             shouldSetBoundaryCallback = true; | ||||
|             factory = repository.fetchContributions(); | ||||
|             factory = repository.fetchContributionsWithStates( | ||||
|                 Collections.singletonList(Contribution.STATE_COMPLETED)); | ||||
|         } | ||||
| 
 | ||||
|         LivePagedListBuilder livePagedListBuilder = new LivePagedListBuilder(factory, pagedListConfig); | ||||
|         LivePagedListBuilder livePagedListBuilder = new LivePagedListBuilder(factory, | ||||
|             pagedListConfig); | ||||
|         if (shouldSetBoundaryCallback) { | ||||
|             livePagedListBuilder.setBoundaryCallback(contributionBoundaryCallback); | ||||
|         } | ||||
|  | @ -89,15 +93,4 @@ public class ContributionsListPresenter implements UserActionListener { | |||
|         contributionBoundaryCallback.dispose(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Delete a failed contribution from the local db | ||||
|      */ | ||||
|     @Override | ||||
|     public void deleteUpload(final Contribution contribution) { | ||||
|         compositeDisposable.add(repository | ||||
|             .deleteContributionFromDB(contribution) | ||||
|             .subscribeOn(ioThreadScheduler) | ||||
|             .subscribe()); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  |  | |||
|  | @ -1,16 +1,14 @@ | |||
| package fr.free.nrw.commons.contributions; | ||||
| 
 | ||||
| import androidx.paging.DataSource.Factory; | ||||
| import fr.free.nrw.commons.kvstore.JsonKvStore; | ||||
| import io.reactivex.Completable; | ||||
| import io.reactivex.Single; | ||||
| import java.util.ArrayList; | ||||
| import java.util.List; | ||||
| 
 | ||||
| import javax.inject.Inject; | ||||
| import javax.inject.Named; | ||||
| 
 | ||||
| import fr.free.nrw.commons.kvstore.JsonKvStore; | ||||
| import io.reactivex.Single; | ||||
| 
 | ||||
| /** | ||||
|  * The LocalDataSource class for Contributions | ||||
|  */ | ||||
|  | @ -43,12 +41,14 @@ class ContributionsLocalDataSource { | |||
| 
 | ||||
|     /** | ||||
|      * Get contribution object from cursor | ||||
|      * | ||||
|      * @param uri | ||||
|      * @return | ||||
|      */ | ||||
|     public Contribution getContributionWithFileName(final String uri) { | ||||
|         final List<Contribution> contributionWithUri = contributionDao.getContributionWithTitle(uri); | ||||
|         if(!contributionWithUri.isEmpty()){ | ||||
|         final List<Contribution> contributionWithUri = contributionDao.getContributionWithTitle( | ||||
|             uri); | ||||
|         if (!contributionWithUri.isEmpty()) { | ||||
|             return contributionWithUri.get(0); | ||||
|         } | ||||
|         return null; | ||||
|  | @ -56,6 +56,7 @@ class ContributionsLocalDataSource { | |||
| 
 | ||||
|     /** | ||||
|      * Remove a contribution from the contributions table | ||||
|      * | ||||
|      * @param contribution | ||||
|      * @return | ||||
|      */ | ||||
|  | @ -63,15 +64,48 @@ class ContributionsLocalDataSource { | |||
|         return contributionDao.delete(contribution); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Deletes contributions with specific states. | ||||
|      * | ||||
|      * @param states The states of the contributions to delete. | ||||
|      * @return A Completable indicating the result of the operation. | ||||
|      */ | ||||
|     public Completable deleteContributionsWithStates(List<Integer> states) { | ||||
|         return contributionDao.deleteContributionsWithStates(states); | ||||
|     } | ||||
| 
 | ||||
|     public Factory<Integer, Contribution> getContributions() { | ||||
|         return contributionDao.fetchContributions(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Fetches contributions with specific states. | ||||
|      * | ||||
|      * @param states The states of the contributions to fetch. | ||||
|      * @return A DataSource factory for paginated contributions with the specified states. | ||||
|      */ | ||||
|     public Factory<Integer, Contribution> getContributionsWithStates(List<Integer> states) { | ||||
|         return contributionDao.getContributions(states); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Fetches contributions with specific states sorted by the date the upload started. | ||||
|      * | ||||
|      * @param states The states of the contributions to fetch. | ||||
|      * @return A DataSource factory for paginated contributions with the specified states sorted by | ||||
|      * date upload started. | ||||
|      */ | ||||
|     public Factory<Integer, Contribution> getContributionsWithStatesSortedByDateUploadStarted( | ||||
|         List<Integer> states) { | ||||
|         return contributionDao.getContributionsSortedByDateUploadStarted(states); | ||||
|     } | ||||
| 
 | ||||
|     public Single<List<Long>> saveContributions(final List<Contribution> contributions) { | ||||
|         final List<Contribution> contributionList = new ArrayList<>(); | ||||
|         for(final Contribution contribution: contributions) { | ||||
|             final Contribution oldContribution = contributionDao.getContribution(contribution.getPageId()); | ||||
|             if(oldContribution != null) { | ||||
|         for (final Contribution contribution : contributions) { | ||||
|             final Contribution oldContribution = contributionDao.getContribution( | ||||
|                 contribution.getPageId()); | ||||
|             if (oldContribution != null) { | ||||
|                 contribution.setWikidataPlace(oldContribution.getWikidataPlace()); | ||||
|             } | ||||
|             contributionList.add(contribution); | ||||
|  | @ -84,10 +118,14 @@ class ContributionsLocalDataSource { | |||
|     } | ||||
| 
 | ||||
|     public void set(final String key, final long value) { | ||||
|         defaultKVStore.putLong(key,value); | ||||
|         defaultKVStore.putLong(key, value); | ||||
|     } | ||||
| 
 | ||||
|     public Completable updateContribution(final Contribution contribution) { | ||||
|         return contributionDao.update(contribution); | ||||
|     } | ||||
| 
 | ||||
|     public Completable updateContributionsWithStates(List<Integer> states, int newState) { | ||||
|         return contributionDao.updateContributionsWithStates(states, newState); | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -1,21 +1,26 @@ | |||
| package fr.free.nrw.commons.contributions; | ||||
| 
 | ||||
| import static fr.free.nrw.commons.utils.ImageUtils.IMAGE_OK; | ||||
| 
 | ||||
| import androidx.work.ExistingWorkPolicy; | ||||
| import fr.free.nrw.commons.MediaDataExtractor; | ||||
| import fr.free.nrw.commons.contributions.ContributionsContract.UserActionListener; | ||||
| import fr.free.nrw.commons.di.CommonsApplicationModule; | ||||
| import fr.free.nrw.commons.repository.UploadRepository; | ||||
| import fr.free.nrw.commons.upload.worker.WorkRequestHelper; | ||||
| import io.reactivex.Scheduler; | ||||
| import io.reactivex.disposables.CompositeDisposable; | ||||
| import javax.inject.Inject; | ||||
| import javax.inject.Named; | ||||
| import timber.log.Timber; | ||||
| 
 | ||||
| /** | ||||
|  * The presenter class for Contributions | ||||
|  */ | ||||
| public class ContributionsPresenter implements UserActionListener { | ||||
| 
 | ||||
|     private final ContributionsRepository repository; | ||||
|     private final ContributionsRepository contributionsRepository; | ||||
|     private final UploadRepository uploadRepository; | ||||
|     private final Scheduler ioThreadScheduler; | ||||
|     private CompositeDisposable compositeDisposable; | ||||
|     private ContributionsContract.View view; | ||||
|  | @ -25,15 +30,17 @@ public class ContributionsPresenter implements UserActionListener { | |||
| 
 | ||||
|     @Inject | ||||
|     ContributionsPresenter(ContributionsRepository repository, | ||||
|         UploadRepository uploadRepository, | ||||
|         @Named(CommonsApplicationModule.IO_THREAD) Scheduler ioThreadScheduler) { | ||||
|         this.repository = repository; | ||||
|         this.ioThreadScheduler=ioThreadScheduler; | ||||
|         this.contributionsRepository = repository; | ||||
|         this.uploadRepository = uploadRepository; | ||||
|         this.ioThreadScheduler = ioThreadScheduler; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onAttachView(ContributionsContract.View view) { | ||||
|         this.view = view; | ||||
|         compositeDisposable=new CompositeDisposable(); | ||||
|         compositeDisposable = new CompositeDisposable(); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|  | @ -44,20 +51,31 @@ public class ContributionsPresenter implements UserActionListener { | |||
| 
 | ||||
|     @Override | ||||
|     public Contribution getContributionsWithTitle(String title) { | ||||
|         return repository.getContributionWithFileName(title); | ||||
|         return contributionsRepository.getContributionWithFileName(title); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Delete a failed contribution from the local db | ||||
|      * @param contribution | ||||
|      * Checks if a contribution is a duplicate and restarts the contribution process if it is not. | ||||
|      * | ||||
|      * @param contribution The contribution to check and potentially restart. | ||||
|      */ | ||||
|     @Override | ||||
|     public void deleteUpload(Contribution contribution) { | ||||
|         compositeDisposable.add(repository | ||||
|     public void checkDuplicateImageAndRestartContribution(Contribution contribution) { | ||||
|         compositeDisposable.add(uploadRepository | ||||
|             .checkDuplicateImage(contribution.getLocalUriPath().getPath()) | ||||
|             .subscribeOn(ioThreadScheduler) | ||||
|             .subscribe(imageCheckResult -> { | ||||
|                 if (imageCheckResult == IMAGE_OK) { | ||||
|                     contribution.setState(Contribution.STATE_QUEUED); | ||||
|                     saveContribution(contribution); | ||||
|                 } else { | ||||
|                     Timber.e("Contribution already exists"); | ||||
|                     compositeDisposable.add(contributionsRepository | ||||
|                         .deleteContributionFromDB(contribution) | ||||
|                         .subscribeOn(ioThreadScheduler) | ||||
|                         .subscribe()); | ||||
|                 } | ||||
|             })); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Update the contribution's state in the databse, upon completion, trigger the workmanager to | ||||
|  | @ -65,9 +83,8 @@ public class ContributionsPresenter implements UserActionListener { | |||
|      * | ||||
|      * @param contribution | ||||
|      */ | ||||
|     @Override | ||||
|     public void saveContribution(Contribution contribution) { | ||||
|         compositeDisposable.add(repository | ||||
|         compositeDisposable.add(contributionsRepository | ||||
|             .save(contribution) | ||||
|             .subscribeOn(ioThreadScheduler) | ||||
|             .subscribe(() -> WorkRequestHelper.Companion.makeOneTimeWorkRequest( | ||||
|  |  | |||
|  | @ -29,6 +29,7 @@ public class ContributionsRepository { | |||
| 
 | ||||
|     /** | ||||
|      * Deletes a failed upload from DB | ||||
|      * | ||||
|      * @param contribution | ||||
|      * @return | ||||
|      */ | ||||
|  | @ -36,8 +37,19 @@ public class ContributionsRepository { | |||
|         return localDataSource.deleteContribution(contribution); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Deletes contributions from the database with specific states. | ||||
|      * | ||||
|      * @param states The states of the contributions to delete. | ||||
|      * @return A Completable indicating the result of the operation. | ||||
|      */ | ||||
|     public Completable deleteContributionsFromDBWithStates(List<Integer> states) { | ||||
|         return localDataSource.deleteContributionsWithStates(states); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get contribution object with title | ||||
|      * | ||||
|      * @param fileName | ||||
|      * @return | ||||
|      */ | ||||
|  | @ -49,19 +61,52 @@ public class ContributionsRepository { | |||
|         return localDataSource.getContributions(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Fetches contributions with specific states. | ||||
|      * | ||||
|      * @param states The states of the contributions to fetch. | ||||
|      * @return A DataSource factory for paginated contributions with the specified states. | ||||
|      */ | ||||
|     public Factory<Integer, Contribution> fetchContributionsWithStates(List<Integer> states) { | ||||
|         return localDataSource.getContributionsWithStates(states); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Fetches contributions with specific states sorted by the date the upload started. | ||||
|      * | ||||
|      * @param states The states of the contributions to fetch. | ||||
|      * @return A DataSource factory for paginated contributions with the specified states sorted by | ||||
|      * date upload started. | ||||
|      */ | ||||
|     public Factory<Integer, Contribution> fetchContributionsWithStatesSortedByDateUploadStarted( | ||||
|         List<Integer> states) { | ||||
|         return localDataSource.getContributionsWithStatesSortedByDateUploadStarted(states); | ||||
|     } | ||||
| 
 | ||||
|     public Single<List<Long>> save(List<Contribution> contributions) { | ||||
|         return localDataSource.saveContributions(contributions); | ||||
|     } | ||||
| 
 | ||||
|     public Completable save(Contribution contributions){ | ||||
|     public Completable save(Contribution contributions) { | ||||
|         return localDataSource.saveContributions(contributions); | ||||
|     } | ||||
| 
 | ||||
|     public void set(String key, long value) { | ||||
|         localDataSource.set(key,value); | ||||
|         localDataSource.set(key, value); | ||||
|     } | ||||
| 
 | ||||
|     public Completable updateContribution(Contribution contribution) { | ||||
|         return localDataSource.updateContribution(contribution); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Updates the state of contributions with specific states. | ||||
|      * | ||||
|      * @param states   The current states of the contributions to update. | ||||
|      * @param newState The new state to set. | ||||
|      * @return A Completable indicating the result of the operation. | ||||
|      */ | ||||
|     public Completable updateContributionsWithStates(List<Integer> states, int newState) { | ||||
|         return localDataSource.updateContributionsWithStates(states, newState); | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -41,11 +41,14 @@ import fr.free.nrw.commons.notification.NotificationController; | |||
| import fr.free.nrw.commons.quiz.QuizChecker; | ||||
| import fr.free.nrw.commons.settings.SettingsFragment; | ||||
| import fr.free.nrw.commons.theme.BaseActivity; | ||||
| import fr.free.nrw.commons.upload.UploadActivity; | ||||
| import fr.free.nrw.commons.upload.UploadProgressActivity; | ||||
| import fr.free.nrw.commons.upload.worker.WorkRequestHelper; | ||||
| import fr.free.nrw.commons.utils.PermissionUtils; | ||||
| import fr.free.nrw.commons.utils.ViewUtilWrapper; | ||||
| import io.reactivex.Completable; | ||||
| import io.reactivex.schedulers.Schedulers; | ||||
| import java.util.Calendar; | ||||
| import java.util.Collections; | ||||
| import java.util.List; | ||||
| import javax.inject.Inject; | ||||
|  | @ -144,16 +147,16 @@ public class MainActivity  extends BaseActivity | |||
|                 applicationKvStore.putBoolean("hasAlreadyLaunchedBigMultiupload", false); | ||||
|                 applicationKvStore.putBoolean("hasAlreadyLaunchedCategoriesDialog", false); | ||||
|             } | ||||
|             if(savedInstanceState == null){ | ||||
|             if (savedInstanceState == null) { | ||||
|                 //starting a fresh fragment. | ||||
|                 // Open Last opened screen if it is Contributions or Nearby, otherwise Contributions | ||||
|                 if(applicationKvStore.getBoolean("last_opened_nearby")){ | ||||
|                 if (applicationKvStore.getBoolean("last_opened_nearby")) { | ||||
|                     setTitle(getString(R.string.nearby_fragment)); | ||||
|                     showNearby(); | ||||
|                     loadFragment(NearbyParentFragment.newInstance(),false); | ||||
|                 }else{ | ||||
|                     loadFragment(NearbyParentFragment.newInstance(), false); | ||||
|                 } else { | ||||
|                     setTitle(getString(R.string.contributions_fragment)); | ||||
|                     loadFragment(ContributionsFragment.newInstance(),false); | ||||
|                     loadFragment(ContributionsFragment.newInstance(), false); | ||||
|                 } | ||||
|             } | ||||
|             setUpPager(); | ||||
|  | @ -165,7 +168,8 @@ public class MainActivity  extends BaseActivity | |||
|             if (VERSION.SDK_INT >= VERSION_CODES.Q) { | ||||
|                 PermissionUtils.checkPermissionsAndPerformAction( | ||||
|                     this, | ||||
|                     () -> {}, | ||||
|                     () -> { | ||||
|                     }, | ||||
|                     R.string.media_location_permission_denied, | ||||
|                     R.string.add_location_manually, | ||||
|                     permission.ACCESS_MEDIA_LOCATION); | ||||
|  | @ -179,7 +183,8 @@ public class MainActivity  extends BaseActivity | |||
|     } | ||||
| 
 | ||||
|     private void setUpPager() { | ||||
|         binding.fragmentMainNavTabLayout.setOnNavigationItemSelectedListener(navListener = (item) -> { | ||||
|         binding.fragmentMainNavTabLayout.setOnNavigationItemSelectedListener( | ||||
|             navListener = (item) -> { | ||||
|                 if (!item.getTitle().equals(getString(R.string.more))) { | ||||
|                     // do not change title for more fragment | ||||
|                     setTitle(item.getTitle()); | ||||
|  | @ -193,18 +198,18 @@ public class MainActivity  extends BaseActivity | |||
|     } | ||||
| 
 | ||||
|     private void setUpLoggedOutPager() { | ||||
|         loadFragment(ExploreFragment.newInstance(),false); | ||||
|         loadFragment(ExploreFragment.newInstance(), false); | ||||
|         binding.fragmentMainNavTabLayout.setOnNavigationItemSelectedListener(item -> { | ||||
|             if (!item.getTitle().equals(getString(R.string.more))) { | ||||
|                 // do not change title for more fragment | ||||
|                 setTitle(item.getTitle()); | ||||
|             } | ||||
|             Fragment fragment = NavTabLoggedOut.of(item.getOrder()).newInstance(); | ||||
|             return loadFragment(fragment,true); | ||||
|             return loadFragment(fragment, true); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     private boolean loadFragment(Fragment fragment,boolean showBottom ) { | ||||
|     private boolean loadFragment(Fragment fragment, boolean showBottom) { | ||||
|         //showBottom so that we do not show the bottom tray again when constructing | ||||
|         //from the saved instance state. | ||||
|         if (fragment instanceof ContributionsFragment) { | ||||
|  | @ -234,7 +239,8 @@ public class MainActivity  extends BaseActivity | |||
|             bookmarkFragment = (BookmarkFragment) fragment; | ||||
|             activeFragment = ActiveFragment.BOOKMARK; | ||||
|         } else if (fragment == null && showBottom) { | ||||
|             if (applicationKvStore.getBoolean("login_skipped") == true) { // If logged out, more sheet is different | ||||
|             if (applicationKvStore.getBoolean("login_skipped") | ||||
|                 == true) { // If logged out, more sheet is different | ||||
|                 MoreBottomSheetLoggedOutFragment bottomSheet = new MoreBottomSheetLoggedOutFragment(); | ||||
|                 bottomSheet.show(getSupportFragmentManager(), | ||||
|                     "MoreBottomSheetLoggedOut"); | ||||
|  | @ -264,28 +270,30 @@ public class MainActivity  extends BaseActivity | |||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Adds number of uploads next to tab text "Contributions" then it will look like | ||||
|      * "Contributions (NUMBER)" | ||||
|      * Adds number of uploads next to tab text "Contributions" then it will look like "Contributions | ||||
|      * (NUMBER)" | ||||
|      * | ||||
|      * @param uploadCount | ||||
|      */ | ||||
|     public void setNumOfUploads(int uploadCount) { | ||||
|         if (activeFragment == ActiveFragment.CONTRIBUTIONS) { | ||||
|             setTitle(getResources().getString(R.string.contributions_fragment) +" "+ ( | ||||
|             setTitle(getResources().getString(R.string.contributions_fragment) + " " + ( | ||||
|                 !(uploadCount == 0) ? | ||||
|                     getResources() | ||||
|                         .getQuantityString(R.plurals.contributions_subtitle, | ||||
|                     uploadCount, uploadCount):getString(R.string.contributions_subtitle_zero))); | ||||
|                             uploadCount, uploadCount) | ||||
|                     : getString(R.string.contributions_subtitle_zero))); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Resume the uploads that got stuck because of the app being killed | ||||
|      * or the device being rebooted. | ||||
|      * | ||||
|      * Resume the uploads that got stuck because of the app being killed or the device being | ||||
|      * rebooted. | ||||
|      * <p> | ||||
|      * When the app is terminated or the device is restarted, contributions remain in the | ||||
|      * 'STATE_IN_PROGRESS' state. This status persists and doesn't change during these events. | ||||
|      * So, retrieving contributions labeled as 'STATE_IN_PROGRESS' | ||||
|      * from the database will provide the list of uploads that appear as stuck on opening the app again | ||||
|      * 'STATE_IN_PROGRESS' state. This status persists and doesn't change during these events. So, | ||||
|      * retrieving contributions labeled as 'STATE_IN_PROGRESS' from the database will provide the | ||||
|      * list of uploads that appear as stuck on opening the app again | ||||
|      */ | ||||
|     @SuppressLint("CheckResult") | ||||
|     private void checkAndResumeStuckUploads() { | ||||
|  | @ -294,9 +302,10 @@ public class MainActivity  extends BaseActivity | |||
|             .subscribeOn(Schedulers.io()) | ||||
|             .blockingGet(); | ||||
|         Timber.d("Resuming " + stuckUploads.size() + " uploads..."); | ||||
|         if(!stuckUploads.isEmpty()) { | ||||
|             for(Contribution contribution: stuckUploads) { | ||||
|         if (!stuckUploads.isEmpty()) { | ||||
|             for (Contribution contribution : stuckUploads) { | ||||
|                 contribution.setState(Contribution.STATE_QUEUED); | ||||
|                 contribution.setDateUploadStarted(Calendar.getInstance().getTime()); | ||||
|                 Completable.fromAction(() -> contributionDao.saveSynchronous(contribution)) | ||||
|                     .subscribeOn(Schedulers.io()) | ||||
|                     .subscribe(); | ||||
|  | @ -323,24 +332,24 @@ public class MainActivity  extends BaseActivity | |||
|     protected void onRestoreInstanceState(Bundle savedInstanceState) { | ||||
|         super.onRestoreInstanceState(savedInstanceState); | ||||
|         String activeFragmentName = savedInstanceState.getString("activeFragment"); | ||||
|         if(activeFragmentName != null) { | ||||
|         if (activeFragmentName != null) { | ||||
|             restoreActiveFragment(activeFragmentName); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private void restoreActiveFragment(@NonNull String fragmentName) { | ||||
|         if(fragmentName.equals(ActiveFragment.CONTRIBUTIONS.name())) { | ||||
|         if (fragmentName.equals(ActiveFragment.CONTRIBUTIONS.name())) { | ||||
|             setTitle(getString(R.string.contributions_fragment)); | ||||
|             loadFragment(ContributionsFragment.newInstance(),false); | ||||
|         }else if(fragmentName.equals(ActiveFragment.NEARBY.name())) { | ||||
|             loadFragment(ContributionsFragment.newInstance(), false); | ||||
|         } else if (fragmentName.equals(ActiveFragment.NEARBY.name())) { | ||||
|             setTitle(getString(R.string.nearby_fragment)); | ||||
|             loadFragment(NearbyParentFragment.newInstance(),false); | ||||
|         }else if(fragmentName.equals(ActiveFragment.EXPLORE.name())) { | ||||
|             loadFragment(NearbyParentFragment.newInstance(), false); | ||||
|         } else if (fragmentName.equals(ActiveFragment.EXPLORE.name())) { | ||||
|             setTitle(getString(R.string.navigation_item_explore)); | ||||
|             loadFragment(ExploreFragment.newInstance(),false); | ||||
|         }else if(fragmentName.equals(ActiveFragment.BOOKMARK.name())) { | ||||
|             loadFragment(ExploreFragment.newInstance(), false); | ||||
|         } else if (fragmentName.equals(ActiveFragment.BOOKMARK.name())) { | ||||
|             setTitle(getString(R.string.bookmarks)); | ||||
|             loadFragment(BookmarkFragment.newInstance(),false); | ||||
|             loadFragment(BookmarkFragment.newInstance(), false); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  | @ -356,8 +365,9 @@ public class MainActivity  extends BaseActivity | |||
|             // Means that nearby fragment is visible | ||||
|             /* If function nearbyParentFragment.backButtonClick() returns false, it means that the bottomsheet is | ||||
|               not expanded. So if the back button is pressed, then go back to the Contributions tab */ | ||||
|             if(!nearbyParentFragment.backButtonClicked()){ | ||||
|                 getSupportFragmentManager().beginTransaction().remove(nearbyParentFragment).commit(); | ||||
|             if (!nearbyParentFragment.backButtonClicked()) { | ||||
|                 getSupportFragmentManager().beginTransaction().remove(nearbyParentFragment) | ||||
|                     .commit(); | ||||
|                 setSelectedItemId(NavTab.CONTRIBUTIONS.code()); | ||||
|             } | ||||
|         } else if (exploreFragment != null && activeFragment == ActiveFragment.EXPLORE) { | ||||
|  | @ -382,18 +392,6 @@ public class MainActivity  extends BaseActivity | |||
|         //initBackButton(); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public boolean onOptionsItemSelected(MenuItem item) { | ||||
|         switch (item.getItemId()) { | ||||
|             case R.id.notifications: | ||||
|                 // Starts notification activity on click to notification icon | ||||
|                 NotificationActivity.startYourself(this, "unread"); | ||||
|                 return true; | ||||
|             default: | ||||
|                 return super.onOptionsItemSelected(item); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Retry all failed uploads as soon as the user returns to the app | ||||
|      */ | ||||
|  | @ -403,31 +401,35 @@ public class MainActivity  extends BaseActivity | |||
|             getContribution(Collections.singletonList(Contribution.STATE_FAILED)) | ||||
|             .subscribeOn(Schedulers.io()) | ||||
|             .subscribe(failedUploads -> { | ||||
|                 for (Contribution contribution: failedUploads) { | ||||
|                 for (Contribution contribution : failedUploads) { | ||||
|                     contributionsFragment.retryUpload(contribution); | ||||
|                 } | ||||
|             }); | ||||
|     } | ||||
| 
 | ||||
|     public void toggleLimitedConnectionMode() { | ||||
|         defaultKvStore.putBoolean(CommonsApplication.IS_LIMITED_CONNECTION_MODE_ENABLED, | ||||
|             !defaultKvStore | ||||
|                 .getBoolean(CommonsApplication.IS_LIMITED_CONNECTION_MODE_ENABLED, false)); | ||||
|         if (defaultKvStore | ||||
|             .getBoolean(CommonsApplication.IS_LIMITED_CONNECTION_MODE_ENABLED, false)) { | ||||
|             viewUtilWrapper | ||||
|                 .showShortToast(getBaseContext(), getString(R.string.limited_connection_enabled)); | ||||
|         } else { | ||||
|             WorkRequestHelper.Companion.makeOneTimeWorkRequest(getApplicationContext(), | ||||
|                 ExistingWorkPolicy.APPEND_OR_REPLACE); | ||||
|             viewUtilWrapper | ||||
|                 .showShortToast(getBaseContext(), getString(R.string.limited_connection_disabled)); | ||||
|     /** | ||||
|      * Handles item selection in the options menu. This method is called when a user interacts with | ||||
|      * the options menu in the Top Bar. | ||||
|      */ | ||||
|     @Override | ||||
|     public boolean onOptionsItemSelected(MenuItem item) { | ||||
|         switch (item.getItemId()) { | ||||
|             case R.id.upload_tab: | ||||
|                 startActivity(new Intent(this, UploadProgressActivity.class)); | ||||
|                 return true; | ||||
|             case R.id.notifications: | ||||
|                 // Starts notification activity on click to notification icon | ||||
|                 NotificationActivity.startYourself(this, "unread"); | ||||
|                 return true; | ||||
|             default: | ||||
|                 return super.onOptionsItemSelected(item); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public void centerMapToPlace(Place place) { | ||||
|         setSelectedItemId(NavTab.NEARBY.code()); | ||||
|         nearbyParentFragment.setNearbyParentFragmentInstanceReadyCallback(new NearbyParentFragmentInstanceReadyCallback() { | ||||
|         nearbyParentFragment.setNearbyParentFragmentInstanceReadyCallback( | ||||
|             new NearbyParentFragmentInstanceReadyCallback() { | ||||
|                 @Override | ||||
|                 public void onReady() { | ||||
|                     nearbyParentFragment.centerMapToPlace(place); | ||||
|  | @ -437,7 +439,7 @@ public class MainActivity  extends BaseActivity | |||
| 
 | ||||
|     @Override | ||||
|     protected void onActivityResult(int requestCode, int resultCode, Intent data) { | ||||
|         Timber.d(data!=null?data.toString():"onActivityResult data is null"); | ||||
|         Timber.d(data != null ? data.toString() : "onActivityResult data is null"); | ||||
|         super.onActivityResult(requestCode, resultCode, data); | ||||
|         controller.handleActivityResult(this, requestCode, resultCode, data); | ||||
|     } | ||||
|  | @ -482,14 +484,15 @@ public class MainActivity  extends BaseActivity | |||
|     /** | ||||
|      * Load default language in onCreate from SharedPreferences | ||||
|      */ | ||||
|     private void loadLocale(){ | ||||
|         final SharedPreferences preferences = getSharedPreferences("Settings", Activity.MODE_PRIVATE); | ||||
|     private void loadLocale() { | ||||
|         final SharedPreferences preferences = getSharedPreferences("Settings", | ||||
|             Activity.MODE_PRIVATE); | ||||
|         final String language = preferences.getString("language", ""); | ||||
|         final SettingsFragment settingsFragment = new SettingsFragment(); | ||||
|         settingsFragment.setLocale(this, language); | ||||
|     } | ||||
| 
 | ||||
|     public NavTabLayout.OnNavigationItemSelectedListener getNavListener(){ | ||||
|     public NavTabLayout.OnNavigationItemSelectedListener getNavListener() { | ||||
|         return navListener; | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -20,6 +20,7 @@ import fr.free.nrw.commons.profile.ProfileActivity; | |||
| import fr.free.nrw.commons.review.ReviewActivity; | ||||
| import fr.free.nrw.commons.settings.SettingsActivity; | ||||
| import fr.free.nrw.commons.upload.UploadActivity; | ||||
| import fr.free.nrw.commons.upload.UploadProgressActivity; | ||||
| 
 | ||||
| /** | ||||
|  * This Class handles the dependency injection (using dagger) | ||||
|  | @ -81,6 +82,9 @@ public abstract class ActivityBuilderModule { | |||
|     @ContributesAndroidInjector | ||||
|     abstract ZoomableActivity bindZoomableActivity(); | ||||
| 
 | ||||
|     @ContributesAndroidInjector | ||||
|     abstract UploadProgressActivity bindUploadProgressActivity(); | ||||
| 
 | ||||
|     @ContributesAndroidInjector | ||||
|     abstract WikidataFeedback bindWikiFeedback(); | ||||
| } | ||||
|  |  | |||
|  | @ -34,6 +34,8 @@ import fr.free.nrw.commons.profile.achievements.AchievementsFragment; | |||
| import fr.free.nrw.commons.profile.leaderboard.LeaderboardFragment; | ||||
| import fr.free.nrw.commons.review.ReviewImageFragment; | ||||
| import fr.free.nrw.commons.settings.SettingsFragment; | ||||
| import fr.free.nrw.commons.upload.FailedUploadsFragment; | ||||
| import fr.free.nrw.commons.upload.PendingUploadsFragment; | ||||
| import fr.free.nrw.commons.upload.categories.UploadCategoriesFragment; | ||||
| import fr.free.nrw.commons.upload.depicts.DepictsFragment; | ||||
| import fr.free.nrw.commons.upload.license.MediaLicenseFragment; | ||||
|  | @ -155,4 +157,10 @@ public abstract class FragmentBuilderModule { | |||
| 
 | ||||
|     @ContributesAndroidInjector | ||||
|     abstract LeaderboardFragment bindLeaderboardFragment(); | ||||
| 
 | ||||
|     @ContributesAndroidInjector | ||||
|     abstract PendingUploadsFragment bindPendingUploadsFragment(); | ||||
| 
 | ||||
|     @ContributesAndroidInjector | ||||
|     abstract FailedUploadsFragment bindFailedUploadsFragment(); | ||||
| } | ||||
|  |  | |||
|  | @ -306,7 +306,6 @@ public class AchievementsFragment extends CommonsDaggerSupportFragment { | |||
|         if (uploadCount==0){ | ||||
|             setZeroAchievements(); | ||||
|         }else { | ||||
| 
 | ||||
|             binding.imagesUploadedProgressbar.setVisibility(View.VISIBLE); | ||||
|             binding.imagesUploadedProgressbar.setProgress | ||||
|                     (100*uploadCount/levelInfo.getMaxUploadCount()); | ||||
|  | @ -326,9 +325,9 @@ public class AchievementsFragment extends CommonsDaggerSupportFragment { | |||
|             getString(R.string.ok), | ||||
|             () -> {}, | ||||
|             true); | ||||
|         binding.imagesUploadedProgressbar.setVisibility(View.INVISIBLE); | ||||
|         binding.imageRevertsProgressbar.setVisibility(View.INVISIBLE); | ||||
|         binding.imagesUsedByWikiProgressBar.setVisibility(View.INVISIBLE); | ||||
| //        binding.imagesUploadedProgressbar.setVisibility(View.INVISIBLE); | ||||
| //        binding.imageRevertsProgressbar.setVisibility(View.INVISIBLE); | ||||
| //        binding.imagesUsedByWikiProgressBar.setVisibility(View.INVISIBLE); | ||||
|         binding.achievementBadgeImage.setVisibility(View.INVISIBLE); | ||||
|         binding.imagesUsedByWikiText.setText(R.string.no_image); | ||||
|         binding.imagesRevertedText.setText(R.string.no_image_reverted); | ||||
|  | @ -354,7 +353,7 @@ public class AchievementsFragment extends CommonsDaggerSupportFragment { | |||
|      * @param achievements | ||||
|      */ | ||||
|     private void inflateAchievements(Achievements achievements) { | ||||
|         binding.imagesUsedByWikiProgressBar.setVisibility(View.VISIBLE); | ||||
| //        binding.imagesUsedByWikiProgressBar.setVisibility(View.VISIBLE); | ||||
|         binding.thanksReceived.setText(String.valueOf(achievements.getThanksReceived())); | ||||
|         binding.imagesUsedByWikiProgressBar.setProgress | ||||
|                 (100 * achievements.getUniqueUsedImages() / levelInfo.getMaxUniqueImages()); | ||||
|  |  | |||
|  | @ -203,6 +203,16 @@ public class UploadRepository { | |||
|         return uploadModel.getImageQuality(uploadItem, location); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * query the RemoteDataSource for image duplicity check | ||||
|      * | ||||
|      * @param filePath file to be checked | ||||
|      * @return IMAGE_DUPLICATE or IMAGE_OK | ||||
|      */ | ||||
|     public Single<Integer> checkDuplicateImage(String filePath) { | ||||
|         return uploadModel.checkDuplicateImage(filePath); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * query the RemoteDataSource for caption quality | ||||
|      * | ||||
|  |  | |||
|  | @ -0,0 +1,139 @@ | |||
| package fr.free.nrw.commons.upload | ||||
| 
 | ||||
| import android.net.Uri | ||||
| import android.text.TextUtils | ||||
| import android.view.LayoutInflater | ||||
| import android.view.View | ||||
| import android.view.ViewGroup | ||||
| import android.webkit.URLUtil | ||||
| import android.widget.ImageView | ||||
| import android.widget.ProgressBar | ||||
| import android.widget.TextView | ||||
| import androidx.paging.PagedListAdapter | ||||
| import androidx.recyclerview.widget.DiffUtil | ||||
| import androidx.recyclerview.widget.RecyclerView | ||||
| import com.facebook.imagepipeline.request.ImageRequest | ||||
| import fr.free.nrw.commons.R | ||||
| import fr.free.nrw.commons.contributions.Contribution | ||||
| import java.io.File | ||||
| 
 | ||||
| /** | ||||
|  * Adapter for displaying failed uploads in a paginated list in FailedUploadsFragment. This adapter | ||||
|  * binds the data from [Contribution] objects to the item views in the RecyclerView, allowing users to view | ||||
|  * details of failed uploads, retry them, or delete them. | ||||
|  * | ||||
|  * @param callback The callback to handle user actions such as Delete Uploads and Restart Uploads | ||||
|  * on failed uploads. | ||||
|  */ | ||||
| class FailedUploadsAdapter(callback: Callback) : | ||||
|     PagedListAdapter<Contribution, FailedUploadsAdapter.ViewHolder>(ContributionDiffCallback()) { | ||||
|     private var callback: Callback = callback | ||||
| 
 | ||||
|     /** | ||||
|      * Creates a new ViewHolder instance. Inflates the layout for each item in the list. | ||||
|      */ | ||||
|     override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { | ||||
|         val view: View = | ||||
|             LayoutInflater.from(parent.context).inflate(R.layout.item_failed_upload, parent, false) | ||||
|         return ViewHolder(view) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Binds data to the provided ViewHolder. Sets up the item view with data from the | ||||
|      * contribution at the specified position. | ||||
|      */ | ||||
|     override fun onBindViewHolder(holder: ViewHolder, position: Int) { | ||||
|         val item: Contribution? = getItem(position) | ||||
|         if (item != null) { | ||||
|             holder.titleTextView.setText(item.media.displayTitle) | ||||
|         } | ||||
|         var imageRequest: ImageRequest? = null | ||||
|         val imageSource: String = item?.localUri.toString() | ||||
| 
 | ||||
|         if (!TextUtils.isEmpty(imageSource)) { | ||||
|             if (URLUtil.isFileUrl(imageSource)) { | ||||
|                 imageRequest = ImageRequest.fromUri(Uri.parse(imageSource))!! | ||||
|             } else if (imageSource != null) { | ||||
|                 val file = File(imageSource) | ||||
|                 imageRequest = ImageRequest.fromFile(file)!! | ||||
|             } | ||||
| 
 | ||||
|             if (imageRequest != null) { | ||||
|                 holder.itemImage.setImageRequest(imageRequest) | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         if (item != null) { | ||||
|             if (item.state == Contribution.STATE_FAILED) { | ||||
|                 if (item.errorInfo != null) { | ||||
|                     holder.errorTextView.setText(item.errorInfo) | ||||
|                 } else { | ||||
|                     holder.errorTextView.setText("Failed") | ||||
|                 } | ||||
|                 holder.errorTextView.visibility = View.VISIBLE | ||||
|                 holder.itemProgress.visibility = View.GONE | ||||
|             } | ||||
|         } | ||||
|         holder.deleteButton.setOnClickListener { | ||||
|             callback.deleteUpload(item) | ||||
|         } | ||||
|         holder.retryButton.setOnClickListener { | ||||
|             callback.restartUpload(position) | ||||
|         } | ||||
|         holder.itemImage.setImageRequest(imageRequest) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * ViewHolder for the failed upload item. Holds references to the views for each item. | ||||
|      */ | ||||
|     class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { | ||||
|         var itemImage: com.facebook.drawee.view.SimpleDraweeView = | ||||
|             itemView.findViewById(R.id.itemImage) | ||||
|         var titleTextView: TextView = itemView.findViewById<TextView>(R.id.titleTextView) | ||||
|         var itemProgress: ProgressBar = itemView.findViewById<ProgressBar>(R.id.itemProgress) | ||||
|         var errorTextView: TextView = itemView.findViewById<TextView>(R.id.errorTextView) | ||||
|         var deleteButton: ImageView = itemView.findViewById(R.id.deleteButton) | ||||
|         var retryButton: ImageView = itemView.findViewById(R.id.retryButton) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Returns the ID of the item at the specified position. Uses the pageId of the contribution | ||||
|      * for unique identification. | ||||
|      */ | ||||
|     override fun getItemId(position: Int): Long { | ||||
|         return getItem(position)?.pageId?.hashCode()?.toLong() ?: position.toLong() | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Uses DiffUtil to calculate the changes in the list | ||||
|      * It has methods that check pageId and the content of the items to determine if its a new item | ||||
|      */ | ||||
|     class ContributionDiffCallback : DiffUtil.ItemCallback<Contribution>() { | ||||
|         override fun areItemsTheSame(oldItem: Contribution, newItem: Contribution): Boolean { | ||||
|             return oldItem.pageId.hashCode() == newItem.pageId.hashCode() | ||||
|         } | ||||
| 
 | ||||
|         override fun areContentsTheSame(oldItem: Contribution, newItem: Contribution): Boolean { | ||||
|             return oldItem.transferred == newItem.transferred | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Callback interface for handling actions related to failed uploads. | ||||
|      */ | ||||
|     interface Callback { | ||||
|         /** | ||||
|          * Deletes the failed upload item. | ||||
|          * | ||||
|          * @param contribution to be deleted. | ||||
|          */ | ||||
|         fun deleteUpload(contribution: Contribution?) | ||||
| 
 | ||||
|         /** | ||||
|          * Restarts the upload for the item at the specified index. | ||||
|          * | ||||
|          * @param index The position of the item in the list. | ||||
|          */ | ||||
|         fun restartUpload(index: Int) | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,201 @@ | |||
| package fr.free.nrw.commons.upload | ||||
| 
 | ||||
| import android.content.Context | ||||
| import android.os.Bundle | ||||
| import android.view.LayoutInflater | ||||
| import android.view.View | ||||
| import android.view.ViewGroup | ||||
| import androidx.fragment.app.Fragment | ||||
| import androidx.paging.PagedList | ||||
| import androidx.recyclerview.widget.LinearLayoutManager | ||||
| import fr.free.nrw.commons.CommonsApplication | ||||
| import fr.free.nrw.commons.R | ||||
| import fr.free.nrw.commons.auth.SessionManager | ||||
| import fr.free.nrw.commons.contributions.Contribution | ||||
| import fr.free.nrw.commons.databinding.FragmentFailedUploadsBinding | ||||
| import fr.free.nrw.commons.di.CommonsDaggerSupportFragment | ||||
| import fr.free.nrw.commons.media.MediaClient | ||||
| import fr.free.nrw.commons.profile.ProfileActivity | ||||
| import fr.free.nrw.commons.utils.DialogUtil | ||||
| import fr.free.nrw.commons.utils.ViewUtil | ||||
| import org.apache.commons.lang3.StringUtils | ||||
| import java.util.Locale | ||||
| import javax.inject.Inject | ||||
| 
 | ||||
| /** | ||||
|  * Fragment for displaying a list of failed uploads in Upload Progress Activity. This fragment provides | ||||
|  * functionality for the user to retry or cancel failed uploads. | ||||
|  */ | ||||
| class FailedUploadsFragment : CommonsDaggerSupportFragment(), PendingUploadsContract.View, | ||||
|     FailedUploadsAdapter.Callback { | ||||
| 
 | ||||
|     @Inject | ||||
|     lateinit var pendingUploadsPresenter: PendingUploadsPresenter | ||||
| 
 | ||||
|     @Inject | ||||
|     lateinit var mediaClient: MediaClient | ||||
| 
 | ||||
|     @Inject | ||||
|     lateinit var sessionManager: SessionManager | ||||
| 
 | ||||
|     private var userName: String? = null | ||||
| 
 | ||||
|     lateinit var binding: FragmentFailedUploadsBinding | ||||
| 
 | ||||
|     private lateinit var adapter: FailedUploadsAdapter | ||||
| 
 | ||||
|     var contributionsList = ArrayList<Contribution>() | ||||
| 
 | ||||
|     private lateinit var uploadProgressActivity: UploadProgressActivity | ||||
| 
 | ||||
|     override fun onAttach(context: Context) { | ||||
|         super.onAttach(context) | ||||
|         if (context is UploadProgressActivity) { | ||||
|             uploadProgressActivity = context | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override fun onCreate(savedInstanceState: Bundle?) { | ||||
|         super.onCreate(savedInstanceState) | ||||
|         //Now that we are allowing this fragment to be started for | ||||
|         // any userName- we expect it to be passed as an argument | ||||
|         if (arguments != null) { | ||||
|             userName = requireArguments().getString(ProfileActivity.KEY_USERNAME) | ||||
|         } | ||||
| 
 | ||||
|         if (StringUtils.isEmpty(userName)) { | ||||
|             userName = sessionManager!!.getUserName() | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override fun onCreateView( | ||||
|         inflater: LayoutInflater, container: ViewGroup?, | ||||
|         savedInstanceState: Bundle? | ||||
|     ): View? { | ||||
|         binding = FragmentFailedUploadsBinding.inflate(layoutInflater) | ||||
|         pendingUploadsPresenter.onAttachView(this) | ||||
|         initAdapter() | ||||
|         return binding.root | ||||
|     } | ||||
| 
 | ||||
|     fun initAdapter() { | ||||
|         adapter = FailedUploadsAdapter(this) | ||||
|     } | ||||
| 
 | ||||
|     override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | ||||
|         super.onViewCreated(view, savedInstanceState) | ||||
|         initRecyclerView() | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Initializes the recycler view. | ||||
|      */ | ||||
|     fun initRecyclerView() { | ||||
|         binding.failedUploadsRecyclerView.setLayoutManager(LinearLayoutManager(this.context)) | ||||
|         binding.failedUploadsRecyclerView.adapter = adapter | ||||
|         pendingUploadsPresenter!!.getFailedContributions() | ||||
|         pendingUploadsPresenter!!.failedContributionList.observe( | ||||
|             viewLifecycleOwner | ||||
|         ) { list: PagedList<Contribution?> -> | ||||
|             adapter.submitList(list) | ||||
|             contributionsList = ArrayList() | ||||
|             list.forEach { | ||||
|                 if (it != null) { | ||||
|                     contributionsList.add(it) | ||||
|                 } | ||||
|             } | ||||
|             if (list.size == 0) { | ||||
|                 uploadProgressActivity.setErrorIconsVisibility(false) | ||||
|                 binding.nofailedTextView.visibility = View.VISIBLE | ||||
|                 binding.failedUplaodsLl.visibility = View.GONE | ||||
|             } else { | ||||
|                 uploadProgressActivity.setErrorIconsVisibility(true) | ||||
|                 binding.nofailedTextView.visibility = View.GONE | ||||
|                 binding.failedUplaodsLl.visibility = View.VISIBLE | ||||
|                 binding.failedUploadsRecyclerView.setAdapter(adapter) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Restarts all the failed uploads. | ||||
|      */ | ||||
|     fun restartUploads() { | ||||
|         if (contributionsList != null) { | ||||
|             pendingUploadsPresenter.restartUploads( | ||||
|                 contributionsList, | ||||
|                 0, | ||||
|                 this.requireContext().applicationContext | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Restarts a specific upload. | ||||
|      */ | ||||
|     override fun restartUpload(index: Int) { | ||||
|         if (contributionsList != null) { | ||||
|             pendingUploadsPresenter.restartUpload( | ||||
|                 contributionsList, | ||||
|                 index, | ||||
|                 this.requireContext().applicationContext | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Deletes a specific upload after getting a confirmation from the user using Dialog. | ||||
|      */ | ||||
|     override fun deleteUpload(contribution: Contribution?) { | ||||
|         DialogUtil.showAlertDialog( | ||||
|             requireActivity(), | ||||
|             String.format( | ||||
|                 Locale.getDefault(), | ||||
|                 requireActivity().getString(R.string.cancelling_upload) | ||||
|             ), | ||||
|             String.format( | ||||
|                 Locale.getDefault(), | ||||
|                 requireActivity().getString(R.string.cancel_upload_dialog) | ||||
|             ), | ||||
|             String.format(Locale.getDefault(), requireActivity().getString(R.string.yes)), | ||||
|             String.format(Locale.getDefault(), requireActivity().getString(R.string.no)), | ||||
|             { | ||||
|                 ViewUtil.showShortToast(context, R.string.cancelling_upload) | ||||
|                 pendingUploadsPresenter.deleteUpload( | ||||
|                     contribution, | ||||
|                     this.requireContext().applicationContext | ||||
|                 ) | ||||
|             }, | ||||
|             {} | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Deletes all the uploads after getting a confirmation from the user using Dialog. | ||||
|      */ | ||||
|     fun deleteUploads() { | ||||
|         if (contributionsList != null) { | ||||
|             DialogUtil.showAlertDialog( | ||||
|                 requireActivity(), | ||||
|                 String.format( | ||||
|                     Locale.getDefault(), | ||||
|                     requireActivity().getString(R.string.cancelling_all_the_uploads) | ||||
|                 ), | ||||
|                 String.format( | ||||
|                     Locale.getDefault(), | ||||
|                     requireActivity().getString(R.string.are_you_sure_that_you_want_cancel_all_the_uploads) | ||||
|                 ), | ||||
|                 String.format(Locale.getDefault(), requireActivity().getString(R.string.yes)), | ||||
|                 String.format(Locale.getDefault(), requireActivity().getString(R.string.no)), | ||||
|                 { | ||||
|                     ViewUtil.showShortToast(context, R.string.cancelling_upload) | ||||
|                     uploadProgressActivity.hidePendingIcons() | ||||
|                     pendingUploadsPresenter.deleteUploads( | ||||
|                         listOf(Contribution.STATE_FAILED) | ||||
|                     ) | ||||
|                 }, | ||||
|                 {} | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | @ -140,7 +140,7 @@ public class ImageProcessingService { | |||
|      * @param filePath file to be checked | ||||
|      * @return IMAGE_DUPLICATE or IMAGE_OK | ||||
|      */ | ||||
|     private Single<Integer> checkDuplicateImage(String filePath) { | ||||
|     Single<Integer> checkDuplicateImage(String filePath) { | ||||
|         Timber.d("Checking for duplicate image %s", filePath); | ||||
|         return Single.fromCallable(() -> fileUtilsWrapper.getFileInputStream(filePath)) | ||||
|             .map(fileUtilsWrapper::getSHA1) | ||||
|  |  | |||
|  | @ -0,0 +1,229 @@ | |||
| package fr.free.nrw.commons.upload | ||||
| 
 | ||||
| import android.net.Uri | ||||
| import android.text.TextUtils | ||||
| import android.view.LayoutInflater | ||||
| import android.view.View | ||||
| import android.view.ViewGroup | ||||
| import android.webkit.URLUtil | ||||
| import android.widget.ImageView | ||||
| import android.widget.ProgressBar | ||||
| import android.widget.TextView | ||||
| import androidx.paging.PagedListAdapter | ||||
| import androidx.recyclerview.widget.DiffUtil | ||||
| import androidx.recyclerview.widget.RecyclerView | ||||
| import com.facebook.imagepipeline.request.ImageRequest | ||||
| import fr.free.nrw.commons.R | ||||
| import fr.free.nrw.commons.contributions.Contribution | ||||
| import timber.log.Timber | ||||
| import java.io.File | ||||
| 
 | ||||
| /** | ||||
|  * Adapter for displaying pending uploads in a paginated list in PendingUploadsFragment. This adapter | ||||
|  * binds data from [Contribution] objects to the item views in the RecyclerView, allowing users to | ||||
|  * view details of pending uploads and perform actions such as deleting them. | ||||
|  * | ||||
|  * @param callback The callback to handle user actions such as Delete Uploads on pending uploads. | ||||
|  */ | ||||
| class PendingUploadsAdapter(private val callback: Callback) : | ||||
|     PagedListAdapter<Contribution, PendingUploadsAdapter.ViewHolder>(ContributionDiffCallback()) { | ||||
| 
 | ||||
|     /** | ||||
|      * Creates a new ViewHolder instance. Inflates the layout for each item in the list. | ||||
|      */ | ||||
|     override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { | ||||
|         val view: View = LayoutInflater.from(parent.context) | ||||
|             .inflate(R.layout.item_pending_upload, parent, false) | ||||
|         return ViewHolder(view) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Binds data to the provided ViewHolder. Sets up the item view with data from the | ||||
|      * contribution at the specified position utilizing payloads. | ||||
|      */ | ||||
|     override fun onBindViewHolder(holder: ViewHolder, position: Int, payloads: MutableList<Any>) { | ||||
|         if (payloads.isNotEmpty()) { | ||||
|             when (val latestPayload = payloads.lastOrNull()) { | ||||
|                 is ContributionChangePayload.Progress -> holder.bindProgress( | ||||
|                     latestPayload.transferred, | ||||
|                     latestPayload.total, | ||||
|                     getItem(position)!!.state | ||||
|                 ) | ||||
| 
 | ||||
|                 is ContributionChangePayload.State -> holder.bindState(latestPayload.state) | ||||
|                 else -> onBindViewHolder(holder, position) | ||||
|             } | ||||
|         } else { | ||||
|             onBindViewHolder(holder, position) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Binds data to the provided ViewHolder. Sets up the item view with data from the | ||||
|      * contribution at the specified position. | ||||
|      */ | ||||
|     override fun onBindViewHolder(holder: ViewHolder, position: Int) { | ||||
|         val contribution = getItem(position) | ||||
|         contribution?.let { | ||||
|             holder.bind(it) | ||||
|             holder.deleteButton.setOnClickListener { | ||||
|                 callback.deleteUpload(contribution) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * ViewHolder class for holding and binding item views. | ||||
|      */ | ||||
|     class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { | ||||
|         var itemImage: com.facebook.drawee.view.SimpleDraweeView = | ||||
|             itemView.findViewById(R.id.itemImage) | ||||
|         var titleTextView: TextView = itemView.findViewById(R.id.titleTextView) | ||||
|         var itemProgress: ProgressBar = itemView.findViewById(R.id.itemProgress) | ||||
|         var errorTextView: TextView = itemView.findViewById(R.id.errorTextView) | ||||
|         var deleteButton: ImageView = itemView.findViewById(R.id.deleteButton) | ||||
| 
 | ||||
|         fun bind(contribution: Contribution) { | ||||
|             titleTextView.text = contribution.media.displayTitle | ||||
| 
 | ||||
|             val imageSource: String = contribution.localUri.toString() | ||||
|             var imageRequest: ImageRequest? = null | ||||
| 
 | ||||
|             if (!TextUtils.isEmpty(imageSource)) { | ||||
|                 if (URLUtil.isFileUrl(imageSource)) { | ||||
|                     imageRequest = ImageRequest.fromUri(Uri.parse(imageSource)) | ||||
|                 } else { | ||||
|                     val file = File(imageSource) | ||||
|                     imageRequest = ImageRequest.fromFile(file) | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             if (imageRequest != null) { | ||||
|                 itemImage.setImageRequest(imageRequest) | ||||
|             } | ||||
| 
 | ||||
|             bindState(contribution.state) | ||||
|             bindProgress(contribution.transferred, contribution.dataLength, contribution.state) | ||||
|         } | ||||
| 
 | ||||
|         fun bindState(state: Int) { | ||||
|             if (state == Contribution.STATE_QUEUED || state == Contribution.STATE_PAUSED) { | ||||
|                 errorTextView.text = "Queued" | ||||
|                 errorTextView.visibility = View.VISIBLE | ||||
|                 itemProgress.visibility = View.GONE | ||||
|             } else { | ||||
|                 errorTextView.visibility = View.GONE | ||||
|                 itemProgress.visibility = View.VISIBLE | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         fun bindProgress(transferred: Long, total: Long, state: Int) { | ||||
|             if (transferred == 0L) { | ||||
|                 errorTextView.text = "Queued" | ||||
|                 errorTextView.visibility = View.VISIBLE | ||||
|                 itemProgress.visibility = View.GONE | ||||
|             } else { | ||||
|                 if (state == Contribution.STATE_QUEUED || state == Contribution.STATE_PAUSED) { | ||||
|                     errorTextView.text = "Queued" | ||||
|                     errorTextView.visibility = View.VISIBLE | ||||
|                     itemProgress.visibility = View.GONE | ||||
|                 } else { | ||||
|                     errorTextView.visibility = View.GONE | ||||
|                     itemProgress.visibility = View.VISIBLE | ||||
|                     if (transferred >= total) { | ||||
|                         itemProgress.isIndeterminate = true | ||||
|                     } else { | ||||
|                         itemProgress.isIndeterminate = false | ||||
|                         itemProgress.progress = | ||||
|                             ((transferred.toDouble() / total.toDouble()) * 100).toInt() | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Callback interface for handling actions related to failed uploads. | ||||
|      */ | ||||
|     interface Callback { | ||||
|         /** | ||||
|          * Deletes the failed upload item. | ||||
|          * | ||||
|          * @param contribution to be deleted. | ||||
|          */ | ||||
|         fun deleteUpload(contribution: Contribution?) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Uses DiffUtil and payloads to calculate the changes in the list | ||||
|      * It has methods that check pageId and the content of the items to determine if its a new item | ||||
|      */ | ||||
|     class ContributionDiffCallback : DiffUtil.ItemCallback<Contribution>() { | ||||
|         /** | ||||
|          * Checks if two items represent the same contribution. | ||||
|          * @param oldItem The old contribution item. | ||||
|          * @param newItem The new contribution item. | ||||
|          * @return True if the items are the same, false otherwise. | ||||
|          */ | ||||
|         override fun areItemsTheSame(oldItem: Contribution, newItem: Contribution): Boolean { | ||||
|             return oldItem.pageId.hashCode() == newItem.pageId.hashCode() | ||||
|         } | ||||
| 
 | ||||
|         /** | ||||
|          * Checks if the content of two items is the same. | ||||
|          * @param oldItem The old contribution item. | ||||
|          * @param newItem The new contribution item. | ||||
|          * @return True if the contents are the same, false otherwise. | ||||
|          */ | ||||
|         override fun areContentsTheSame(oldItem: Contribution, newItem: Contribution): Boolean { | ||||
|             return oldItem.transferred == newItem.transferred | ||||
|         } | ||||
| 
 | ||||
|         /** | ||||
|          * Returns a payload representing the change between the old and new items. | ||||
|          * @param oldItem The old contribution item. | ||||
|          * @param newItem The new contribution item. | ||||
|          * @return An object representing the change, or null if there are no changes. | ||||
|          */ | ||||
|         override fun getChangePayload(oldItem: Contribution, newItem: Contribution): Any? { | ||||
|             return when { | ||||
|                 oldItem.transferred != newItem.transferred -> { | ||||
|                     ContributionChangePayload.Progress(newItem.transferred, newItem.dataLength) | ||||
|                 } | ||||
| 
 | ||||
|                 oldItem.state != newItem.state -> { | ||||
|                     ContributionChangePayload.State(newItem.state) | ||||
|                 } | ||||
| 
 | ||||
|                 else -> super.getChangePayload(oldItem, newItem) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Returns the unique item ID for the contribution at the specified position. | ||||
|      * @param position The position of the item. | ||||
|      * @return The unique item ID. | ||||
|      */ | ||||
|     override fun getItemId(position: Int): Long { | ||||
|         return getItem(position)?.pageId?.hashCode()?.toLong() ?: position.toLong() | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Sealed interface representing different types of changes to a contribution. | ||||
|      */ | ||||
|     private sealed interface ContributionChangePayload { | ||||
|         /** | ||||
|          * Represents a change in the progress of a contribution. | ||||
|          * @param transferred The amount of data transferred. | ||||
|          * @param total The total amount of data. | ||||
|          */ | ||||
|         data class Progress(val transferred: Long, val total: Long) : ContributionChangePayload | ||||
| 
 | ||||
|         /** | ||||
|          * Represents a change in the state of a contribution. | ||||
|          * @param state The state of the contribution. | ||||
|          */ | ||||
|         data class State(val state: Int) : ContributionChangePayload | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,31 @@ | |||
| package fr.free.nrw.commons.upload; | ||||
| 
 | ||||
| import android.content.Context; | ||||
| import fr.free.nrw.commons.BasePresenter; | ||||
| import fr.free.nrw.commons.contributions.Contribution; | ||||
| import fr.free.nrw.commons.nearby.contract.NearbyParentFragmentContract; | ||||
| import fr.free.nrw.commons.nearby.contract.NearbyParentFragmentContract.View; | ||||
| 
 | ||||
| /** | ||||
|  * The contract using which the PendingUploadsFragment or FailedUploadsFragment would communicate | ||||
|  * with its PendingUploadsPresenter | ||||
|  */ | ||||
| public class PendingUploadsContract { | ||||
| 
 | ||||
|     /** | ||||
|      * Interface representing the view for uploads. | ||||
|      */ | ||||
|     public interface View { } | ||||
| 
 | ||||
|     /** | ||||
|      * Interface representing the user actions related to uploads. | ||||
|      */ | ||||
|     public interface UserActionListener extends | ||||
|         BasePresenter<fr.free.nrw.commons.upload.PendingUploadsContract.View> { | ||||
| 
 | ||||
|         /** | ||||
|          * Deletes a upload. | ||||
|          */ | ||||
|         void deleteUpload(Contribution contribution, Context context); | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,200 @@ | |||
| package fr.free.nrw.commons.upload | ||||
| 
 | ||||
| import android.content.Context | ||||
| import android.os.AsyncTask | ||||
| import android.os.Build.VERSION | ||||
| import android.os.Build.VERSION_CODES | ||||
| import android.os.Bundle | ||||
| import android.view.LayoutInflater | ||||
| import android.view.View | ||||
| import android.view.ViewGroup | ||||
| import androidx.paging.PagedList | ||||
| import androidx.paging.PositionalDataSource | ||||
| import androidx.recyclerview.widget.LinearLayoutManager | ||||
| import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver | ||||
| import fr.free.nrw.commons.CommonsApplication | ||||
| import fr.free.nrw.commons.R | ||||
| import fr.free.nrw.commons.auth.SessionManager | ||||
| import fr.free.nrw.commons.contributions.Contribution | ||||
| import fr.free.nrw.commons.databinding.FragmentPendingUploadsBinding | ||||
| import fr.free.nrw.commons.di.CommonsDaggerSupportFragment | ||||
| import fr.free.nrw.commons.media.MediaClient | ||||
| import fr.free.nrw.commons.profile.ProfileActivity | ||||
| import fr.free.nrw.commons.utils.DialogUtil.showAlertDialog | ||||
| import fr.free.nrw.commons.utils.ViewUtil | ||||
| import org.apache.commons.lang3.StringUtils | ||||
| import timber.log.Timber | ||||
| import java.util.Locale | ||||
| import javax.inject.Inject | ||||
| 
 | ||||
| /** | ||||
|  * Fragment for showing pending uploads in Upload Progress Activity. This fragment provides | ||||
|  * functionality for the user to pause uploads. | ||||
|  */ | ||||
| class PendingUploadsFragment : CommonsDaggerSupportFragment(), PendingUploadsContract.View, | ||||
|     PendingUploadsAdapter.Callback { | ||||
| 
 | ||||
|     @Inject | ||||
|     lateinit var pendingUploadsPresenter: PendingUploadsPresenter | ||||
| 
 | ||||
|     private lateinit var binding: FragmentPendingUploadsBinding | ||||
| 
 | ||||
|     private lateinit var uploadProgressActivity: UploadProgressActivity | ||||
| 
 | ||||
|     private lateinit var adapter: PendingUploadsAdapter | ||||
| 
 | ||||
|     private var contributionsSize = 0 | ||||
|     var contributionsList = ArrayList<Contribution>() | ||||
| 
 | ||||
|     override fun onAttach(context: Context) { | ||||
|         super.onAttach(context) | ||||
|         if (context is UploadProgressActivity) { | ||||
|             uploadProgressActivity = context | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override fun onCreateView( | ||||
|         inflater: LayoutInflater, container: ViewGroup?, | ||||
|         savedInstanceState: Bundle? | ||||
|     ): View? { | ||||
|         super.onCreate(savedInstanceState) | ||||
|         binding = FragmentPendingUploadsBinding.inflate(inflater, container, false) | ||||
|         pendingUploadsPresenter.onAttachView(this) | ||||
|         initAdapter() | ||||
|         return binding.root | ||||
|     } | ||||
| 
 | ||||
|     fun initAdapter() { | ||||
|         adapter = PendingUploadsAdapter(this) | ||||
|     } | ||||
| 
 | ||||
|     override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | ||||
|         super.onViewCreated(view, savedInstanceState) | ||||
|         initRecyclerView() | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Initializes the recycler view. | ||||
|      */ | ||||
|     fun initRecyclerView() { | ||||
|         binding.pendingUploadsRecyclerView.setLayoutManager(LinearLayoutManager(this.context)) | ||||
|         binding.pendingUploadsRecyclerView.adapter = adapter | ||||
|         pendingUploadsPresenter!!.setup() | ||||
|         pendingUploadsPresenter!!.totalContributionList.observe( | ||||
|             viewLifecycleOwner | ||||
|         ) { list: PagedList<Contribution?> -> | ||||
|             contributionsSize = list.size | ||||
|             contributionsList = ArrayList() | ||||
|             var pausedOrQueuedUploads = 0 | ||||
|             list.forEach { | ||||
|                 if (it != null) { | ||||
|                     if (it.state == Contribution.STATE_PAUSED | ||||
|                         || it.state == Contribution.STATE_QUEUED | ||||
|                         || it.state == Contribution.STATE_IN_PROGRESS | ||||
|                     ) { | ||||
|                         contributionsList.add(it) | ||||
|                     } | ||||
|                     if (it.state == Contribution.STATE_PAUSED | ||||
|                         || it.state == Contribution.STATE_QUEUED | ||||
|                     ) { | ||||
|                         pausedOrQueuedUploads++ | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             if (contributionsSize == 0) { | ||||
|                 binding.nopendingTextView.visibility = View.VISIBLE | ||||
|                 binding.pendingUplaodsLl.visibility = View.GONE | ||||
|                 uploadProgressActivity.hidePendingIcons() | ||||
|             } else { | ||||
|                 binding.nopendingTextView.visibility = View.GONE | ||||
|                 binding.pendingUplaodsLl.visibility = View.VISIBLE | ||||
|                 adapter.submitList(list) | ||||
|                 binding.progressTextView.setText(contributionsSize.toString() + " uploads left") | ||||
|                 if ((pausedOrQueuedUploads == contributionsSize) || CommonsApplication.isPaused) { | ||||
|                     uploadProgressActivity.setPausedIcon(true) | ||||
|                 } else { | ||||
|                     uploadProgressActivity.setPausedIcon(false) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Cancels a specific upload after getting a confirmation from the user using Dialog. | ||||
|      */ | ||||
|     override fun deleteUpload(contribution: Contribution?) { | ||||
|         showAlertDialog( | ||||
|             requireActivity(), | ||||
|             String.format( | ||||
|                 Locale.getDefault(), | ||||
|                 requireActivity().getString(R.string.cancelling_upload) | ||||
|             ), | ||||
|             String.format( | ||||
|                 Locale.getDefault(), | ||||
|                 requireActivity().getString(R.string.cancel_upload_dialog) | ||||
|             ), | ||||
|             String.format(Locale.getDefault(), requireActivity().getString(R.string.yes)), | ||||
|             String.format(Locale.getDefault(), requireActivity().getString(R.string.no)), | ||||
|             { | ||||
|                 ViewUtil.showShortToast(context, R.string.cancelling_upload) | ||||
|                 pendingUploadsPresenter.deleteUpload( | ||||
|                     contribution, | ||||
|                     this.requireContext().applicationContext | ||||
|                 ) | ||||
|             }, | ||||
|             {} | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Restarts all the paused uploads. | ||||
|      */ | ||||
|     fun restartUploads() { | ||||
|         if (contributionsList != null) { | ||||
|             pendingUploadsPresenter.restartUploads( | ||||
|                 contributionsList, | ||||
|                 0, | ||||
|                 this.requireContext().applicationContext | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Pauses all the ongoing uploads. | ||||
|      */ | ||||
|     fun pauseUploads() { | ||||
|         pendingUploadsPresenter.pauseUploads() | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Cancels all the uploads after getting a confirmation from the user using Dialog. | ||||
|      */ | ||||
|     fun deleteUploads() { | ||||
|         showAlertDialog( | ||||
|             requireActivity(), | ||||
|             String.format( | ||||
|                 Locale.getDefault(), | ||||
|                 requireActivity().getString(R.string.cancelling_all_the_uploads) | ||||
|             ), | ||||
|             String.format( | ||||
|                 Locale.getDefault(), | ||||
|                 requireActivity().getString(R.string.are_you_sure_that_you_want_cancel_all_the_uploads) | ||||
|             ), | ||||
|             String.format(Locale.getDefault(), requireActivity().getString(R.string.yes)), | ||||
|             String.format(Locale.getDefault(), requireActivity().getString(R.string.no)), | ||||
|             { | ||||
|                 ViewUtil.showShortToast(context, R.string.cancelling_upload) | ||||
|                 uploadProgressActivity.hidePendingIcons() | ||||
|                 pendingUploadsPresenter.deleteUploads( | ||||
|                     listOf( | ||||
|                         Contribution.STATE_QUEUED, | ||||
|                         Contribution.STATE_IN_PROGRESS, | ||||
|                         Contribution.STATE_PAUSED | ||||
|                     ) | ||||
|                 ) | ||||
|             }, | ||||
|             {} | ||||
|         ) | ||||
| 
 | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,262 @@ | |||
| package fr.free.nrw.commons.upload; | ||||
| 
 | ||||
| 
 | ||||
| import static fr.free.nrw.commons.utils.ImageUtils.IMAGE_OK; | ||||
| 
 | ||||
| import android.content.Context; | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.lifecycle.LiveData; | ||||
| import androidx.paging.DataSource.Factory; | ||||
| import androidx.paging.LivePagedListBuilder; | ||||
| import androidx.paging.PagedList; | ||||
| import androidx.work.ExistingWorkPolicy; | ||||
| import fr.free.nrw.commons.CommonsApplication; | ||||
| import fr.free.nrw.commons.contributions.Contribution; | ||||
| import fr.free.nrw.commons.contributions.ContributionBoundaryCallback; | ||||
| import fr.free.nrw.commons.contributions.ContributionsRemoteDataSource; | ||||
| import fr.free.nrw.commons.contributions.ContributionsRepository; | ||||
| import fr.free.nrw.commons.di.CommonsApplicationModule; | ||||
| import fr.free.nrw.commons.repository.UploadRepository; | ||||
| import fr.free.nrw.commons.upload.PendingUploadsContract.UserActionListener; | ||||
| import fr.free.nrw.commons.upload.PendingUploadsContract.View; | ||||
| import fr.free.nrw.commons.upload.worker.WorkRequestHelper; | ||||
| import io.reactivex.Scheduler; | ||||
| import io.reactivex.disposables.CompositeDisposable; | ||||
| import java.util.ArrayList; | ||||
| import java.util.Arrays; | ||||
| import java.util.Calendar; | ||||
| import java.util.Collections; | ||||
| import java.util.List; | ||||
| import javax.inject.Inject; | ||||
| import javax.inject.Named; | ||||
| import timber.log.Timber; | ||||
| 
 | ||||
| /** | ||||
|  * The presenter class for PendingUploadsFragment and FailedUploadsFragment | ||||
|  */ | ||||
| public class PendingUploadsPresenter implements UserActionListener { | ||||
| 
 | ||||
|     private final ContributionBoundaryCallback contributionBoundaryCallback; | ||||
|     private final ContributionsRepository contributionsRepository; | ||||
|     private final UploadRepository uploadRepository; | ||||
|     private final Scheduler ioThreadScheduler; | ||||
| 
 | ||||
|     private final CompositeDisposable compositeDisposable; | ||||
|     private final ContributionsRemoteDataSource contributionsRemoteDataSource; | ||||
| 
 | ||||
|     LiveData<PagedList<Contribution>> totalContributionList; | ||||
|     LiveData<PagedList<Contribution>> failedContributionList; | ||||
| 
 | ||||
|     @Inject | ||||
|     PendingUploadsPresenter( | ||||
|         final ContributionBoundaryCallback contributionBoundaryCallback, | ||||
|         final ContributionsRemoteDataSource contributionsRemoteDataSource, | ||||
|         final ContributionsRepository contributionsRepository, | ||||
|         final UploadRepository uploadRepository, | ||||
|         @Named(CommonsApplicationModule.IO_THREAD) final Scheduler ioThreadScheduler) { | ||||
|         this.contributionBoundaryCallback = contributionBoundaryCallback; | ||||
|         this.contributionsRepository = contributionsRepository; | ||||
|         this.uploadRepository = uploadRepository; | ||||
|         this.ioThreadScheduler = ioThreadScheduler; | ||||
|         this.contributionsRemoteDataSource = contributionsRemoteDataSource; | ||||
|         compositeDisposable = new CompositeDisposable(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Setups the paged list of Pending Uploads. This method sets the configuration for paged list | ||||
|      * and ties it up with the live data object. This method can be tweaked to update the lazy | ||||
|      * loading behavior of the contributions list | ||||
|      */ | ||||
|     void setup() { | ||||
|         final PagedList.Config pagedListConfig = | ||||
|             (new PagedList.Config.Builder()) | ||||
|                 .setPrefetchDistance(50) | ||||
|                 .setPageSize(10).build(); | ||||
|         Factory<Integer, Contribution> factory; | ||||
| 
 | ||||
|         factory = contributionsRepository.fetchContributionsWithStatesSortedByDateUploadStarted( | ||||
|             Arrays.asList(Contribution.STATE_QUEUED, Contribution.STATE_IN_PROGRESS, | ||||
|                 Contribution.STATE_PAUSED)); | ||||
|         LivePagedListBuilder livePagedListBuilder = new LivePagedListBuilder(factory, | ||||
|             pagedListConfig); | ||||
|         totalContributionList = livePagedListBuilder.build(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Setups the paged list of Failed Uploads. This method sets the configuration for paged list | ||||
|      * and ties it up with the live data object. This method can be tweaked to update the lazy | ||||
|      * loading behavior of the contributions list | ||||
|      */ | ||||
|     void getFailedContributions() { | ||||
|         final PagedList.Config pagedListConfig = | ||||
|             (new PagedList.Config.Builder()) | ||||
|                 .setPrefetchDistance(50) | ||||
|                 .setPageSize(10).build(); | ||||
|         Factory<Integer, Contribution> factory; | ||||
|         factory = contributionsRepository.fetchContributionsWithStatesSortedByDateUploadStarted( | ||||
|             Collections.singletonList(Contribution.STATE_FAILED)); | ||||
|         LivePagedListBuilder livePagedListBuilder = new LivePagedListBuilder(factory, | ||||
|             pagedListConfig); | ||||
|         failedContributionList = livePagedListBuilder.build(); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onAttachView(@NonNull View view) { | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onDetachView() { | ||||
|         compositeDisposable.clear(); | ||||
|         contributionsRemoteDataSource.dispose(); | ||||
|         contributionBoundaryCallback.dispose(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Deletes the specified upload (contribution) from the database. | ||||
|      * | ||||
|      * @param contribution The contribution object representing the upload to be deleted. | ||||
|      * @param context      The context in which the operation is being performed. | ||||
|      */ | ||||
|     @Override | ||||
|     public void deleteUpload(final Contribution contribution, Context context) { | ||||
|         compositeDisposable.add(contributionsRepository | ||||
|             .deleteContributionFromDB(contribution) | ||||
|             .subscribeOn(ioThreadScheduler) | ||||
|             .subscribe()); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Pauses all the uploads by changing the state of contributions from STATE_QUEUED and | ||||
|      * STATE_IN_PROGRESS to STATE_PAUSED in the database. | ||||
|      */ | ||||
|     public void pauseUploads() { | ||||
|         CommonsApplication.isPaused = true; | ||||
|         compositeDisposable.add(contributionsRepository | ||||
|             .updateContributionsWithStates( | ||||
|                 List.of(Contribution.STATE_QUEUED, Contribution.STATE_IN_PROGRESS), | ||||
|                 Contribution.STATE_PAUSED) | ||||
|             .subscribeOn(ioThreadScheduler) | ||||
|             .subscribe()); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Deletes contributions from the database that match the specified states. | ||||
|      * | ||||
|      * @param states A list of integers representing the states of the contributions to be deleted. | ||||
|      */ | ||||
|     public void deleteUploads(List<Integer> states) { | ||||
|         compositeDisposable.add(contributionsRepository | ||||
|             .deleteContributionsFromDBWithStates(states) | ||||
|             .subscribeOn(ioThreadScheduler) | ||||
|             .subscribe()); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Restarts the uploads for the specified list of contributions starting from the given index. | ||||
|      * | ||||
|      * @param contributionList The list of contributions to be restarted. | ||||
|      * @param index            The starting index in the list from which to restart uploads. | ||||
|      * @param context          The context in which the operation is being performed. | ||||
|      */ | ||||
|     public void restartUploads(List<Contribution> contributionList, int index, Context context) { | ||||
|         CommonsApplication.isPaused = false; | ||||
|         if (index >= contributionList.size()) { | ||||
|             return; | ||||
|         } | ||||
|         Contribution it = contributionList.get(index); | ||||
|         if (it.getState() == Contribution.STATE_FAILED) { | ||||
|             it.setDateUploadStarted(Calendar.getInstance().getTime()); | ||||
|             if (it.getErrorInfo() == null) { | ||||
|                 it.setChunkInfo(null); | ||||
|                 it.setTransferred(0); | ||||
|             } | ||||
|             compositeDisposable.add(uploadRepository | ||||
|                 .checkDuplicateImage(it.getLocalUriPath().getPath()) | ||||
|                 .subscribeOn(ioThreadScheduler) | ||||
|                 .subscribe(imageCheckResult -> { | ||||
|                     if (imageCheckResult == IMAGE_OK) { | ||||
|                         it.setState(Contribution.STATE_QUEUED); | ||||
|                         compositeDisposable.add(contributionsRepository | ||||
|                             .save(it) | ||||
|                             .subscribeOn(ioThreadScheduler) | ||||
|                             .doOnComplete(() -> { | ||||
|                                 restartUploads(contributionList, index + 1, context); | ||||
|                             }) | ||||
|                             .subscribe(() -> WorkRequestHelper.Companion.makeOneTimeWorkRequest( | ||||
|                                 context, ExistingWorkPolicy.KEEP))); | ||||
|                     } else { | ||||
|                         Timber.e("Contribution already exists"); | ||||
|                         compositeDisposable.add(contributionsRepository | ||||
|                             .deleteContributionFromDB(it) | ||||
|                             .subscribeOn(ioThreadScheduler).doOnComplete(() -> { | ||||
|                                 restartUploads(contributionList, index + 1, context); | ||||
|                             }) | ||||
|                             .subscribe()); | ||||
|                     } | ||||
|                 }, throwable -> { | ||||
|                     Timber.e(throwable); | ||||
|                     restartUploads(contributionList, index + 1, context); | ||||
|                 })); | ||||
|         } else { | ||||
|             it.setState(Contribution.STATE_QUEUED); | ||||
|             compositeDisposable.add(contributionsRepository | ||||
|                 .save(it) | ||||
|                 .subscribeOn(ioThreadScheduler) | ||||
|                 .doOnComplete(() -> { | ||||
|                     restartUploads(contributionList, index + 1, context); | ||||
|                 }) | ||||
|                 .subscribe(() -> WorkRequestHelper.Companion.makeOneTimeWorkRequest( | ||||
|                     context, ExistingWorkPolicy.KEEP))); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Restarts the upload for the specified list of contributions for the given index. | ||||
|      * | ||||
|      * @param contributionList The list of contributions. | ||||
|      * @param index            The index in the list which to be restarted. | ||||
|      * @param context          The context in which the operation is being performed. | ||||
|      */ | ||||
|     public void restartUpload(List<Contribution> contributionList, int index, Context context) { | ||||
|         CommonsApplication.isPaused = false; | ||||
|         if (index >= contributionList.size()) { | ||||
|             return; | ||||
|         } | ||||
|         Contribution it = contributionList.get(index); | ||||
|         if (it.getState() == Contribution.STATE_FAILED) { | ||||
|             it.setDateUploadStarted(Calendar.getInstance().getTime()); | ||||
|             if (it.getErrorInfo() == null) { | ||||
|                 it.setChunkInfo(null); | ||||
|                 it.setTransferred(0); | ||||
|             } | ||||
|             compositeDisposable.add(uploadRepository | ||||
|                 .checkDuplicateImage(it.getLocalUriPath().getPath()) | ||||
|                 .subscribeOn(ioThreadScheduler) | ||||
|                 .subscribe(imageCheckResult -> { | ||||
|                     if (imageCheckResult == IMAGE_OK) { | ||||
|                         it.setState(Contribution.STATE_QUEUED); | ||||
|                         compositeDisposable.add(contributionsRepository | ||||
|                             .save(it) | ||||
|                             .subscribeOn(ioThreadScheduler) | ||||
|                             .subscribe(() -> WorkRequestHelper.Companion.makeOneTimeWorkRequest( | ||||
|                                 context, ExistingWorkPolicy.KEEP))); | ||||
|                     } else { | ||||
|                         Timber.e("Contribution already exists"); | ||||
|                         compositeDisposable.add(contributionsRepository | ||||
|                             .deleteContributionFromDB(it) | ||||
|                             .subscribeOn(ioThreadScheduler) | ||||
|                             .subscribe()); | ||||
|                     } | ||||
|                 })); | ||||
|         } else { | ||||
|             it.setState(Contribution.STATE_QUEUED); | ||||
|             compositeDisposable.add(contributionsRepository | ||||
|                 .save(it) | ||||
|                 .subscribeOn(ioThreadScheduler) | ||||
|                 .subscribe(() -> WorkRequestHelper.Companion.makeOneTimeWorkRequest( | ||||
|                     context, ExistingWorkPolicy.KEEP))); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | @ -9,5 +9,6 @@ data class StashUploadResult( | |||
| enum class StashUploadState { | ||||
|     SUCCESS, | ||||
|     PAUSED, | ||||
|     FAILED | ||||
|     FAILED, | ||||
|     CANCELLED | ||||
| } | ||||
|  |  | |||
|  | @ -6,6 +6,7 @@ import fr.free.nrw.commons.CommonsApplication | |||
| import fr.free.nrw.commons.auth.csrf.CsrfTokenClient | ||||
| import fr.free.nrw.commons.contributions.ChunkInfo | ||||
| import fr.free.nrw.commons.contributions.Contribution | ||||
| import fr.free.nrw.commons.contributions.ContributionDao | ||||
| import fr.free.nrw.commons.upload.worker.UploadWorker.NotificationUpdateProgressListener | ||||
| import fr.free.nrw.commons.wikidata.mwapi.MwException | ||||
| import io.reactivex.Observable | ||||
|  | @ -33,7 +34,8 @@ class UploadClient @Inject constructor( | |||
|     private val csrfTokenClient: CsrfTokenClient, | ||||
|     private val pageContentsCreator: PageContentsCreator, | ||||
|     private val fileUtilsWrapper: FileUtilsWrapper, | ||||
|     private val gson: Gson, private val timeProvider: TimeProvider | ||||
|     private val gson: Gson, private val timeProvider: TimeProvider, | ||||
|     private val contributionDao: ContributionDao | ||||
| ) { | ||||
|     private val CHUNK_SIZE = 512 * 1024 // 512 KB | ||||
| 
 | ||||
|  | @ -58,8 +60,6 @@ class UploadClient @Inject constructor( | |||
|             ) | ||||
|         } | ||||
| 
 | ||||
|         contribution.unpause() | ||||
| 
 | ||||
|         val file = contribution.localUriPath | ||||
|         val fileChunks = fileUtilsWrapper.getFileChunks(file, CHUNK_SIZE) | ||||
|         val mediaType = fileUtilsWrapper.getMimeType(file).toMediaTypeOrNull() | ||||
|  | @ -79,17 +79,35 @@ class UploadClient @Inject constructor( | |||
|         val errorMessage = AtomicReference<String>() | ||||
|         compositeDisposable.add( | ||||
|             Observable.fromIterable(fileChunks).forEach { chunkFile: File -> | ||||
|                 if (canProcess(contribution, failures)) { | ||||
|                 if (canProcess(contributionDao, contribution, failures)) { | ||||
|                     if (contributionDao.getContribution(contribution.pageId) == null) { | ||||
|                         compositeDisposable.clear() | ||||
|                         return@forEach | ||||
|                     } else { | ||||
|                         processChunk( | ||||
|                         filename, contribution, notificationUpdater, chunkFile, | ||||
|                         failures, chunkInfo, index, errorMessage, mediaType!!, file!!, fileChunks.size | ||||
|                             filename, | ||||
|                             contribution, | ||||
|                             notificationUpdater, | ||||
|                             chunkFile, | ||||
|                             failures, | ||||
|                             chunkInfo, | ||||
|                             index, | ||||
|                             errorMessage, | ||||
|                             mediaType!!, | ||||
|                             file!!, | ||||
|                             fileChunks.size | ||||
|                         ) | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         ) | ||||
| 
 | ||||
|         return when { | ||||
|             contribution.isPaused() -> { | ||||
|             contributionDao.getContribution(contribution.pageId) == null -> { | ||||
|                 return Observable.just(StashUploadResult(StashUploadState.CANCELLED, null, "Upload cancelled")) | ||||
|             } | ||||
|             contributionDao.getContribution(contribution.pageId).state == Contribution.STATE_PAUSED | ||||
|                     || CommonsApplication.isPaused -> { | ||||
|                 Timber.d("Upload stash paused %s", contribution.pageId) | ||||
|                 Observable.just(StashUploadResult(StashUploadState.PAUSED, null, null)) | ||||
|             } | ||||
|  | @ -248,10 +266,15 @@ class UploadClient @Inject constructor( | |||
|     } | ||||
| } | ||||
| 
 | ||||
| private fun canProcess(contribution: Contribution, failures: AtomicBoolean): Boolean { | ||||
| private fun canProcess( | ||||
|     contributionDao: ContributionDao, | ||||
|     contribution: Contribution, | ||||
|     failures: AtomicBoolean | ||||
| ): Boolean { | ||||
|     // As long as the contribution hasn't been paused and there are no errors, | ||||
|     // we can process the current chunk. | ||||
|     return !(contribution.isPaused() || failures.get()) | ||||
|     return !(contributionDao.getContribution(contribution.pageId).state == Contribution.STATE_PAUSED | ||||
|             || failures.get() || CommonsApplication.isPaused) | ||||
| } | ||||
| 
 | ||||
| private fun shouldSkip( | ||||
|  |  | |||
|  | @ -103,6 +103,16 @@ public class UploadModel { | |||
|         return imageProcessingService.validateImage(uploadItem, inAppPictureLocation); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Calls checkDuplicateImage() of ImageProcessingService to check if image is duplicate | ||||
|      * | ||||
|      * @param filePath file to be checked | ||||
|      * @return IMAGE_DUPLICATE or IMAGE_OK | ||||
|      */ | ||||
|     public Single<Integer> checkDuplicateImage(String filePath){ | ||||
|         return imageProcessingService.checkDuplicateImage(filePath); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Calls validateCaption() of ImageProcessingService to check caption of image | ||||
|      * | ||||
|  |  | |||
|  | @ -5,6 +5,7 @@ import dagger.Binds; | |||
| import dagger.Module; | ||||
| import dagger.Provides; | ||||
| import fr.free.nrw.commons.auth.csrf.CsrfTokenClient; | ||||
| import fr.free.nrw.commons.contributions.ContributionDao; | ||||
| import fr.free.nrw.commons.di.NetworkingModule; | ||||
| import fr.free.nrw.commons.upload.categories.CategoriesContract; | ||||
| import fr.free.nrw.commons.upload.categories.CategoriesPresenter; | ||||
|  | @ -50,8 +51,8 @@ public abstract class UploadModule { | |||
|     public static UploadClient provideUploadClient(final UploadInterface uploadInterface, | ||||
|         @Named(NetworkingModule.NAMED_COMMONS_CSRF) final CsrfTokenClient csrfTokenClient, | ||||
|         final PageContentsCreator pageContentsCreator, final FileUtilsWrapper fileUtilsWrapper, | ||||
|         final Gson gson) { | ||||
|         final Gson gson, final ContributionDao contributionDao) { | ||||
|         return new UploadClient(uploadInterface, csrfTokenClient, pageContentsCreator, | ||||
|             fileUtilsWrapper, gson, System::currentTimeMillis); | ||||
|             fileUtilsWrapper, gson, System::currentTimeMillis, contributionDao); | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -0,0 +1,223 @@ | |||
| package fr.free.nrw.commons.upload | ||||
| 
 | ||||
| import android.annotation.SuppressLint | ||||
| import android.os.Bundle | ||||
| import android.view.Menu | ||||
| import android.view.MenuItem | ||||
| import androidx.fragment.app.Fragment | ||||
| import androidx.viewpager.widget.ViewPager | ||||
| import fr.free.nrw.commons.R | ||||
| import fr.free.nrw.commons.ViewPagerAdapter | ||||
| import fr.free.nrw.commons.contributions.Contribution | ||||
| import fr.free.nrw.commons.contributions.ContributionDao | ||||
| import fr.free.nrw.commons.databinding.ActivityUploadProgressBinding | ||||
| import fr.free.nrw.commons.theme.BaseActivity | ||||
| import io.reactivex.functions.Consumer | ||||
| import io.reactivex.schedulers.Schedulers | ||||
| import timber.log.Timber | ||||
| import javax.inject.Inject | ||||
| 
 | ||||
| /** | ||||
|  * Activity to manage the progress of uploads. It includes tabs to show pending and failed uploads, | ||||
|  * and provides menu options to pause, resume, cancel, and retry uploads. Also, it contains ViewPager | ||||
|  * which holds Pending Uploads Fragment and Failed Uploads Fragment to show list of pending and | ||||
|  * failed uploads respectively. | ||||
|  */ | ||||
| class UploadProgressActivity : BaseActivity() { | ||||
| 
 | ||||
|     private lateinit var binding: ActivityUploadProgressBinding | ||||
|     private var pendingUploadsFragment: PendingUploadsFragment? = null | ||||
|     private var failedUploadsFragment: FailedUploadsFragment? = null | ||||
|     var viewPagerAdapter: ViewPagerAdapter? = null | ||||
|     var menu: Menu? = null | ||||
| 
 | ||||
|     @Inject | ||||
|     lateinit var contributionDao: ContributionDao | ||||
| 
 | ||||
|     val fragmentList: MutableList<Fragment> = ArrayList() | ||||
|     val titleList: MutableList<String> = ArrayList() | ||||
|     var isPaused = true | ||||
|     var isPendingIconsVisible = true | ||||
|     var isErrorIconsVisisble = false | ||||
| 
 | ||||
|     override fun onCreate(savedInstanceState: Bundle?) { | ||||
|         super.onCreate(savedInstanceState) | ||||
|         binding = ActivityUploadProgressBinding.inflate(layoutInflater) | ||||
|         setContentView(binding.root) | ||||
|         viewPagerAdapter = ViewPagerAdapter(supportFragmentManager) | ||||
|         binding.uploadProgressViewPager.setAdapter(viewPagerAdapter) | ||||
|         binding.uploadProgressViewPager.setId(R.id.upload_progress_view_pager) | ||||
|         binding.uploadProgressTabLayout.setupWithViewPager(binding.uploadProgressViewPager) | ||||
|         binding.toolbarBinding.toolbar.title = getString(R.string.uploads) | ||||
|         setSupportActionBar(binding.toolbarBinding.toolbar) | ||||
|         supportActionBar?.setDisplayHomeAsUpEnabled(true) | ||||
| 
 | ||||
|         binding.uploadProgressViewPager.addOnPageChangeListener(object : | ||||
|             ViewPager.OnPageChangeListener { | ||||
|             override fun onPageScrolled( | ||||
|                 position: Int, positionOffset: Float, | ||||
|                 positionOffsetPixels: Int | ||||
|             ) { | ||||
|             } | ||||
| 
 | ||||
|             override fun onPageSelected(position: Int) { | ||||
|                 updateMenuItems(position) | ||||
|                 if (position == 2) { | ||||
|                     binding.uploadProgressViewPager.setCanScroll(false) | ||||
|                 } else { | ||||
|                     binding.uploadProgressViewPager.setCanScroll(true) | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             override fun onPageScrollStateChanged(state: Int) { | ||||
|             } | ||||
|         }) | ||||
|         setTabs() | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Initializes and sets up the tabs data by creating instances of `PendingUploadsFragment` | ||||
|      * and `FailedUploadsFragment`, adds them to the `fragmentList`, and assigns corresponding | ||||
|      * titles from resources to the `titleList`. | ||||
|      */ | ||||
|     fun setTabs() { | ||||
|         pendingUploadsFragment = PendingUploadsFragment() | ||||
|         failedUploadsFragment = FailedUploadsFragment() | ||||
| 
 | ||||
|         fragmentList.add(pendingUploadsFragment!!) | ||||
|         titleList.add(getString(R.string.pending)) | ||||
|         fragmentList.add(failedUploadsFragment!!) | ||||
|         titleList.add(getString(R.string.failed)) | ||||
|         viewPagerAdapter!!.setTabData(fragmentList, titleList) | ||||
|         viewPagerAdapter!!.notifyDataSetChanged() | ||||
|     } | ||||
| 
 | ||||
|     override fun onCreateOptionsMenu(menu: Menu?): Boolean { | ||||
|         menuInflater.inflate(R.menu.menu_uploads, menu) | ||||
|         this.menu = menu | ||||
|         updateMenuItems(0) | ||||
|         return super.onCreateOptionsMenu(menu) | ||||
|     } | ||||
| 
 | ||||
|     override fun onSupportNavigateUp(): Boolean { | ||||
|         onBackPressed() | ||||
|         return true | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Updates the menu items based on the current position in the view pager and the visibility | ||||
|      * of icons related to pending or failed uploads. This function dynamically modifies the menu | ||||
|      * to display pause, resume, retry, and cancel options depending on the state of the uploads. | ||||
|      * | ||||
|      * @param currentPosition The current position in the view pager. A value of `0` indicates | ||||
|      * pending uploads, while `1` indicates failed uploads. | ||||
|      */ | ||||
|     fun updateMenuItems(currentPosition: Int) { | ||||
|         if (menu != null) { | ||||
|             menu!!.clear() | ||||
|             if (currentPosition == 0) { | ||||
|                 if (isPendingIconsVisible) { | ||||
|                     if (!isPaused) { | ||||
|                         if (menu!!.findItem(R.id.pause_icon) == null) { | ||||
|                             menu!!.add( | ||||
|                                 Menu.NONE, | ||||
|                                 R.id.pause_icon, | ||||
|                                 Menu.NONE, | ||||
|                                 getString(R.string.pause) | ||||
|                             ) | ||||
|                                 .setIcon(R.drawable.pause_icon) | ||||
|                                 .setOnMenuItemClickListener { | ||||
|                                     pendingUploadsFragment!!.pauseUploads() | ||||
|                                     setPausedIcon(true) | ||||
|                                     true | ||||
|                                 } | ||||
|                                 .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM) | ||||
|                         } | ||||
|                         if (menu!!.findItem(R.id.cancel_icon) == null) { | ||||
|                             menu!!.add( | ||||
|                                 Menu.NONE, | ||||
|                                 R.id.cancel_icon, | ||||
|                                 Menu.NONE, | ||||
|                                 getString(R.string.cancel) | ||||
|                             ) | ||||
|                                 .setIcon(R.drawable.ic_cancel_upload) | ||||
|                                 .setOnMenuItemClickListener { | ||||
|                                     pendingUploadsFragment!!.deleteUploads() | ||||
|                                     true | ||||
|                                 } | ||||
|                                 .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM) | ||||
|                         } | ||||
|                     } else { | ||||
|                         if (menu!!.findItem(R.id.resume_icon) == null) { | ||||
|                             menu!!.add( | ||||
|                                 Menu.NONE, | ||||
|                                 R.id.resume_icon, | ||||
|                                 Menu.NONE, | ||||
|                                 getString(R.string.resume) | ||||
|                             ) | ||||
|                                 .setIcon(R.drawable.play_icon) | ||||
|                                 .setOnMenuItemClickListener { | ||||
|                                     pendingUploadsFragment!!.restartUploads() | ||||
|                                     setPausedIcon(false) | ||||
|                                     true | ||||
|                                 } | ||||
|                                 .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM) | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } else if (currentPosition == 1) { | ||||
|                 if (isErrorIconsVisisble) { | ||||
|                     if (menu!!.findItem(R.id.retry_icon) == null) { | ||||
|                         menu!!.add(Menu.NONE, R.id.retry_icon, Menu.NONE, getString(R.string.retry)) | ||||
|                             .setIcon(R.drawable.ic_refresh_24dp).setOnMenuItemClickListener { | ||||
|                                 failedUploadsFragment!!.restartUploads() | ||||
|                                 true | ||||
|                             } | ||||
|                             .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM) | ||||
|                     } | ||||
|                     if (menu!!.findItem(R.id.cancel_icon) == null) { | ||||
|                         menu!!.add( | ||||
|                             Menu.NONE, | ||||
|                             R.id.cancel_icon, | ||||
|                             Menu.NONE, | ||||
|                             getString(R.string.cancel) | ||||
|                         ) | ||||
|                             .setIcon(R.drawable.ic_cancel_upload) | ||||
|                             .setOnMenuItemClickListener { | ||||
|                                 failedUploadsFragment!!.deleteUploads() | ||||
|                                 true | ||||
|                             } | ||||
|                             .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM) | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Hides the menu icons related to pending uploads. | ||||
|      */ | ||||
|     fun hidePendingIcons() { | ||||
|         isPendingIconsVisible = false | ||||
|         updateMenuItems(binding.uploadProgressViewPager.currentItem) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Sets the paused state and updates the menu items accordingly. | ||||
|      * @param paused A boolean indicating whether all the uploads are paused. | ||||
|      */ | ||||
|     fun setPausedIcon(paused: Boolean) { | ||||
|         isPaused = paused | ||||
|         updateMenuItems(binding.uploadProgressViewPager.currentItem) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Sets the visibility of the menu icons related to failed uploads. | ||||
|      * @param visible A boolean indicating whether the error icons should be visible. | ||||
|      */ | ||||
|     fun setErrorIconsVisibility(visible: Boolean) { | ||||
|         isErrorIconsVisisble = visible | ||||
|         updateMenuItems(binding.uploadProgressViewPager.currentItem) | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | @ -34,13 +34,11 @@ import fr.free.nrw.commons.upload.FileUtilsWrapper | |||
| import fr.free.nrw.commons.upload.StashUploadResult | ||||
| import fr.free.nrw.commons.upload.StashUploadState | ||||
| import fr.free.nrw.commons.upload.UploadClient | ||||
| import fr.free.nrw.commons.upload.UploadProgressActivity | ||||
| import fr.free.nrw.commons.upload.UploadResult | ||||
| import fr.free.nrw.commons.wikidata.WikidataEditService | ||||
| import kotlinx.coroutines.Dispatchers | ||||
| import kotlinx.coroutines.MainScope | ||||
| import kotlinx.coroutines.flow.asFlow | ||||
| import kotlinx.coroutines.flow.collect | ||||
| import kotlinx.coroutines.flow.map | ||||
| import kotlinx.coroutines.launch | ||||
| import kotlinx.coroutines.withContext | ||||
| import timber.log.Timber | ||||
|  | @ -106,7 +104,6 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) : | |||
|             getNotificationBuilder(CommonsApplication.NOTIFICATION_CHANNEL_ID_ALL)!! | ||||
| 
 | ||||
|         statesToProcess.add(Contribution.STATE_QUEUED) | ||||
|         statesToProcess.add(Contribution.STATE_QUEUED_LIMITED_CONNECTION_MODE) | ||||
|     } | ||||
| 
 | ||||
|     @dagger.Module | ||||
|  | @ -166,7 +163,8 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) : | |||
|     } | ||||
| 
 | ||||
|     override suspend fun doWork(): Result { | ||||
|         var countUpload = 0 | ||||
|         try { | ||||
|             var totalUploadsStarted = 0 | ||||
|             // Start a foreground service | ||||
|             setForeground(createForegroundInfo()) | ||||
|             notificationManager = NotificationManagerCompat.from(appContext) | ||||
|  | @ -174,6 +172,13 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) : | |||
|                 CommonsApplication.NOTIFICATION_CHANNEL_ID_ALL | ||||
|             )!! | ||||
|             withContext(Dispatchers.IO) { | ||||
|                 while (contributionDao.getContribution(statesToProcess) | ||||
|                         .blockingGet().size > 0 && contributionDao.getContribution( | ||||
|                         arrayListOf( | ||||
|                             Contribution.STATE_IN_PROGRESS | ||||
|                         ) | ||||
|                     ).blockingGet().size == 0 | ||||
|                 ) { | ||||
|                     /* | ||||
|                     queuedContributions receives the results from a one-shot query. | ||||
|                     This means that once the list has been fetched from the database, | ||||
|  | @ -188,8 +193,6 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) : | |||
|                         .blockingGet() | ||||
|                     //Showing initial notification for the number of uploads being processed | ||||
| 
 | ||||
|             Timber.e("Queued Contributions: " + queuedContributions.size) | ||||
| 
 | ||||
|                     processingUploads.setContentTitle(appContext.getString(R.string.starting_uploads)) | ||||
|                     processingUploads.setContentText( | ||||
|                         appContext.resources.getQuantityString( | ||||
|  | @ -204,45 +207,20 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) : | |||
|                         processingUploads.build() | ||||
|                     ) | ||||
| 
 | ||||
|             /** | ||||
|              * To avoid race condition when multiple of these workers are working, assign this state | ||||
|             so that the next one does not process these contribution again | ||||
|              */ | ||||
|             queuedContributions.forEach { | ||||
|                 it.state = Contribution.STATE_IN_PROGRESS | ||||
|                 contributionDao.saveSynchronous(it) | ||||
|             } | ||||
|                     val sortedQueuedContributionsList: List<Contribution> = | ||||
|                         queuedContributions.sortedBy { it.dateUploadStartedInMillis() } | ||||
| 
 | ||||
|             queuedContributions.asFlow().map { contribution -> | ||||
|                 // Upload the contribution if it has not been cancelled by the user | ||||
|                 if (!CommonsApplication.cancelledUploads.contains(contribution.pageId)) { | ||||
|                     /** | ||||
|                      * If the limited connection mode is on, lets iterate through the queued | ||||
|                      * contributions | ||||
|                      * and set the state as STATE_QUEUED_LIMITED_CONNECTION_MODE , | ||||
|                      * otherwise proceed with the upload | ||||
|                      */ | ||||
|                     if (isLimitedConnectionModeEnabled()) { | ||||
|                         if (contribution.state == Contribution.STATE_QUEUED) { | ||||
|                             contribution.state = Contribution.STATE_QUEUED_LIMITED_CONNECTION_MODE | ||||
|                             contributionDao.saveSynchronous(contribution) | ||||
|                         } | ||||
|                     } else { | ||||
|                     var contribution = sortedQueuedContributionsList.first() | ||||
| 
 | ||||
|                     if (contributionDao.getContribution(contribution.pageId) != null) { | ||||
|                         contribution.transferred = 0 | ||||
|                         contribution.state = Contribution.STATE_IN_PROGRESS | ||||
|                         contributionDao.saveSynchronous(contribution) | ||||
|                         setProgressAsync(Data.Builder().putInt("progress", countUpload).build()) | ||||
|                         countUpload++ | ||||
|                         setProgressAsync(Data.Builder().putInt("progress", totalUploadsStarted).build()) | ||||
|                         totalUploadsStarted++ | ||||
|                         uploadContribution(contribution = contribution) | ||||
|                     } | ||||
|                 } else { | ||||
|                     /* We can remove the cancelled upload from the hashset | ||||
|                        as this contribution will not be processed again | ||||
|                      */ | ||||
|                     removeUploadFromInMemoryHashSet(contribution) | ||||
|                 } | ||||
|             }.collect() | ||||
| 
 | ||||
|                 //Dismiss the global notification | ||||
|                 notificationManager?.cancel( | ||||
|                     PROCESSING_UPLOADS_NOTIFICATION_TAG, | ||||
|  | @ -258,13 +236,12 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) : | |||
|             } | ||||
| 
 | ||||
|             return Result.success() | ||||
|         } catch (e: Exception) { | ||||
|             Timber.e(e, "UploadWorker encountered an error.") | ||||
|             return Result.failure() | ||||
|         } finally { | ||||
|             WorkRequestHelper.markUploadWorkerAsStopped() | ||||
|         } | ||||
| 
 | ||||
|     /** | ||||
|      * Removes the processed contribution from the cancelledUploads in-memory hashset | ||||
|      */ | ||||
|     private fun removeUploadFromInMemoryHashSet(contribution: Contribution) { | ||||
|         CommonsApplication.cancelledUploads.remove(contribution.pageId) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | @ -287,12 +264,6 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) : | |||
|             .setContentTitle(appContext.getString(R.string.upload_in_progress)) | ||||
|             .build() | ||||
|     } | ||||
|     /** | ||||
|      * Returns true is the limited connection mode is enabled | ||||
|      */ | ||||
|     private fun isLimitedConnectionModeEnabled(): Boolean { | ||||
|         return sessionManager.getPreference(CommonsApplication.IS_LIMITED_CONNECTION_MODE_ENABLED) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Upload the contribution | ||||
|  | @ -343,7 +314,6 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) : | |||
|             ).onErrorReturn{ | ||||
|                 return@onErrorReturn StashUploadResult(StashUploadState.FAILED,fileKey = null,errorMessage = it.message) | ||||
|             }.blockingSingle() | ||||
| 
 | ||||
|             when (stashUploadResult.state) { | ||||
|                 StashUploadState.SUCCESS -> { | ||||
|                     //If the stash upload succeeds | ||||
|  | @ -403,14 +373,19 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) : | |||
|                     contribution.state = Contribution.STATE_PAUSED | ||||
|                     contributionDao.saveSynchronous(contribution) | ||||
|                 } | ||||
|                 StashUploadState.CANCELLED -> { | ||||
|                     showCancelledNotification(contribution) | ||||
|                 } | ||||
|                 else -> { | ||||
|                     Timber.e("""upload file to stash failed with status: ${stashUploadResult.state}""") | ||||
|                     showInvalidLoginNotification(contribution) | ||||
|                     contribution.state = Contribution.STATE_FAILED | ||||
|                     contribution.chunkInfo = null | ||||
|                     contribution.errorInfo = stashUploadResult.errorMessage | ||||
|                     showErrorNotification(contribution) | ||||
|                     contributionDao.saveSynchronous(contribution) | ||||
|                     if (stashUploadResult.errorMessage.equals(CsrfTokenClient.INVALID_TOKEN_ERROR_MESSAGE)) { | ||||
|                         Timber.e("Invalid Login, logging out") | ||||
|                         showInvalidLoginNotification(contribution) | ||||
|                         val username = sessionManager.userName | ||||
|                         var logoutListener = CommonsApplication.BaseLogoutListener( | ||||
|                             appContext, | ||||
|  | @ -426,6 +401,7 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) : | |||
|             Timber.e(exception) | ||||
|             Timber.e("Stash upload failed for contribution: $filename") | ||||
|             showFailedNotification(contribution) | ||||
|             contribution.errorInfo=exception.message | ||||
|             contribution.state=Contribution.STATE_FAILED | ||||
|             clearChunks(contribution) | ||||
|         } | ||||
|  | @ -543,6 +519,7 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) : | |||
|     private fun showSuccessNotification(contribution: Contribution) { | ||||
|         val displayTitle = contribution.media.displayTitle | ||||
|         contribution.state=Contribution.STATE_COMPLETED | ||||
|         curentNotification.setContentIntent(getPendingIntent(MainActivity::class.java)) | ||||
|         curentNotification.setContentTitle( | ||||
|             appContext.getString( | ||||
|                 R.string.upload_completed_notification_title, | ||||
|  | @ -565,7 +542,7 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) : | |||
|     @SuppressLint("StringFormatInvalid") | ||||
|     private fun showFailedNotification(contribution: Contribution) { | ||||
|         val displayTitle = contribution.media.displayTitle | ||||
|         curentNotification.setContentIntent(getPendingIntent(MainActivity::class.java)) | ||||
|         curentNotification.setContentIntent(getPendingIntent(UploadProgressActivity::class.java)) | ||||
|         curentNotification.setContentTitle( | ||||
|             appContext.getString( | ||||
|                 R.string.upload_failed_notification_title, | ||||
|  | @ -598,12 +575,34 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) : | |||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Shows a notification for a failed contribution upload. | ||||
|      */ | ||||
|     @SuppressLint("StringFormatInvalid") | ||||
|     private fun showErrorNotification(contribution: Contribution) { | ||||
|         val displayTitle = contribution.media.displayTitle | ||||
|         curentNotification.setContentTitle( | ||||
|             appContext.getString( | ||||
|                 R.string.upload_failed_notification_title, | ||||
|                 displayTitle | ||||
|             ) | ||||
|         ) | ||||
|             .setContentText(contribution.errorInfo) | ||||
|             .setProgress(0, 0, false) | ||||
|             .setOngoing(false) | ||||
|         notificationManager?.notify( | ||||
|             currentNotificationTag, currentNotificationID, | ||||
|             curentNotification.build() | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Notify that the current upload is paused | ||||
|      * @param contribution | ||||
|      */ | ||||
|     private fun showPausedNotification(contribution: Contribution) { | ||||
|         val displayTitle = contribution.media.displayTitle | ||||
|         curentNotification.setContentIntent(getPendingIntent(UploadProgressActivity::class.java)) | ||||
|         curentNotification.setContentTitle( | ||||
|             appContext.getString( | ||||
|                 R.string.upload_paused_notification_title, | ||||
|  | @ -619,6 +618,25 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) : | |||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Notify that the current upload is cancelled | ||||
|      * @param contribution | ||||
|      */ | ||||
|     private fun showCancelledNotification(contribution: Contribution) { | ||||
|         val displayTitle = contribution.media.displayTitle | ||||
|         curentNotification.setContentIntent(getPendingIntent(UploadProgressActivity::class.java)) | ||||
|         curentNotification.setContentTitle( | ||||
|             displayTitle | ||||
|         ) | ||||
|             .setContentText("Upload has been cancelled!") | ||||
|             .setProgress(0, 0, false) | ||||
|             .setOngoing(false) | ||||
|         notificationManager!!.notify( | ||||
|             currentNotificationTag, currentNotificationID, | ||||
|             curentNotification.build() | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Method used to get Pending intent for opening different screen after clicking on notification | ||||
|      * @param toClass | ||||
|  |  | |||
|  | @ -3,6 +3,7 @@ package fr.free.nrw.commons.upload.worker | |||
| import android.content.Context | ||||
| import androidx.work.* | ||||
| import androidx.work.WorkRequest.Companion.MIN_BACKOFF_MILLIS | ||||
| import timber.log.Timber | ||||
| import java.util.concurrent.TimeUnit | ||||
| 
 | ||||
| /** | ||||
|  | @ -11,7 +12,22 @@ import java.util.concurrent.TimeUnit | |||
| class WorkRequestHelper { | ||||
| 
 | ||||
|     companion object { | ||||
| 
 | ||||
|         private var isUploadWorkerRunning = false | ||||
|         private val lock = Object() | ||||
| 
 | ||||
|         fun makeOneTimeWorkRequest(context: Context, existingWorkPolicy: ExistingWorkPolicy) { | ||||
| 
 | ||||
|             synchronized(lock) { | ||||
|                 if (isUploadWorkerRunning) { | ||||
|                     Timber.e("UploadWorker is already running. Cannot start another instance.") | ||||
|                     return | ||||
|                 } else { | ||||
|                     Timber.e("Setting isUploadWorkerRunning to true") | ||||
|                     isUploadWorkerRunning = true | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             /* Set backoff criteria for the work request | ||||
|            The default backoff policy is EXPONENTIAL, but while testing we found that it | ||||
|            too long for the uploads to finish. So, set the backoff policy as LINEAR with the | ||||
|  | @ -35,7 +51,17 @@ class WorkRequestHelper { | |||
|             WorkManager.getInstance(context).enqueueUniqueWork( | ||||
|                 UploadWorker::class.java.simpleName, existingWorkPolicy, uploadRequest | ||||
|             ) | ||||
|         } | ||||
| 
 | ||||
|         } | ||||
| 
 | ||||
|         /** | ||||
|          * Sets the flag isUploadWorkerRunning to`false` allowing new worker to be started. | ||||
|          */ | ||||
|         fun markUploadWorkerAsStopped() { | ||||
|             synchronized(lock) { | ||||
|                 isUploadWorkerRunning = false | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										17
									
								
								app/src/main/res/drawable/ic_cancel_upload.xml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								app/src/main/res/drawable/ic_cancel_upload.xml
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,17 @@ | |||
| <?xml version="1.0" encoding="UTF-8" standalone="no"?> | ||||
| <vector xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|   android:width="24dp" | ||||
|   android:height="24dp" | ||||
|   android:tint="?attr/mediaDetailsHeadingText" | ||||
|   android:viewportWidth="24.0" | ||||
|   android:viewportHeight="24.0"> | ||||
|   <group | ||||
|     android:scaleX="1.44427" | ||||
|     android:scaleY="1.44427" | ||||
|     android:translateX="-5.33124" | ||||
|     android:translateY="-5.33124"> | ||||
|     <path | ||||
|       android:fillColor="@android:color/white" | ||||
|       android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z" /> | ||||
|   </group> | ||||
| </vector> | ||||
							
								
								
									
										14
									
								
								app/src/main/res/drawable/ic_refresh_24dp.xml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								app/src/main/res/drawable/ic_refresh_24dp.xml
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,14 @@ | |||
| <vector xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     android:width="@dimen/half_standard_height" | ||||
|     android:height="@dimen/half_standard_height" | ||||
|     android:viewportHeight="24.0" | ||||
|     android:viewportWidth="24.0"> | ||||
|     <group android:scaleX="1.44427" | ||||
|       android:scaleY="1.44427" | ||||
|       android:translateX="-5.33124" | ||||
|       android:translateY="-5.33124"> | ||||
|     <path | ||||
|         android:fillColor="?attr/mediaDetailsHeadingText" | ||||
|         android:pathData="M17.65,6.35C16.2,4.9 14.21,4 12,4c-4.42,0 -7.99,3.58 -7.99,8s3.57,8 7.99,8c3.73,0 6.84,-2.55 7.73,-6h-2.08c-0.82,2.33 -3.04,4 -5.65,4 -3.31,0 -6,-2.69 -6,-6s2.69,-6 6,-6c1.66,0 3.14,0.69 4.22,1.78L13,11h7V4l-2.35,2.35z"/> | ||||
|     </group> | ||||
| </vector> | ||||
|  | @ -5,7 +5,7 @@ | |||
|     android:viewportWidth="24.0"> | ||||
| 
 | ||||
|     <path | ||||
|         android:fillColor="#FFFFFFFF" | ||||
|         android:fillColor="@color/white" | ||||
|         android:pathData="M17.65,6.35C16.2,4.9 14.21,4 12,4c-4.42,0 -7.99,3.58 -7.99,8s3.57,8 7.99,8c3.73,0 6.84,-2.55 7.73,-6h-2.08c-0.82,2.33 -3.04,4 -5.65,4 -3.31,0 -6,-2.69 -6,-6s2.69,-6 6,-6c1.66,0 3.14,0.69 4.22,1.78L13,11h7V4l-2.35,2.35z"/> | ||||
| 
 | ||||
| </vector> | ||||
|  |  | |||
							
								
								
									
										11
									
								
								app/src/main/res/drawable/ic_upload_blue_24dp.xml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								app/src/main/res/drawable/ic_upload_blue_24dp.xml
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,11 @@ | |||
| <vector xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|   android:width="24dp" | ||||
|   android:height="24dp" | ||||
|   android:viewportWidth="24" | ||||
|   android:viewportHeight="24"> | ||||
| 
 | ||||
|   <path | ||||
|     android:fillColor="@color/primaryDarkColor" | ||||
|     android:pathData="M5,20h14v-2H5V20zM5,8h4v8h6v-8h4l-7,-7L5,8z" /> | ||||
| 
 | ||||
| </vector> | ||||
							
								
								
									
										12
									
								
								app/src/main/res/drawable/ic_upload_white_24dp.xml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								app/src/main/res/drawable/ic_upload_white_24dp.xml
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,12 @@ | |||
| <vector xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|   android:width="24dp" | ||||
|   android:height="24dp" | ||||
|   android:tint="#FFFFFF" | ||||
|   android:viewportWidth="24" | ||||
|   android:viewportHeight="24"> | ||||
| 
 | ||||
|   <path | ||||
|     android:fillColor="@android:color/white" | ||||
|     android:pathData="M5,20h14v-2H5V20zM5,8h4v8h6v-8h4l-7,-7L5,8z" /> | ||||
| 
 | ||||
| </vector> | ||||
							
								
								
									
										48
									
								
								app/src/main/res/layout/activity_upload_progress.xml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								app/src/main/res/layout/activity_upload_progress.xml
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,48 @@ | |||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|   xmlns:app="http://schemas.android.com/apk/res-auto" | ||||
|   xmlns:tools="http://schemas.android.com/tools" | ||||
|   android:layout_width="match_parent" | ||||
|   android:layout_height="match_parent" | ||||
|   android:orientation="vertical" | ||||
|   tools:context=".upload.UploadProgressActivity"> | ||||
| 
 | ||||
| 
 | ||||
|   <include | ||||
|     android:id="@+id/toolbarBinding" | ||||
|     layout="@layout/toolbar" | ||||
|     android:layout_width="match_parent" | ||||
|     android:layout_height="?attr/actionBarSize"/> | ||||
| 
 | ||||
|   <RelativeLayout | ||||
|     android:layout_width="match_parent" | ||||
|     android:layout_height="match_parent"> | ||||
| 
 | ||||
|     <com.google.android.material.appbar.AppBarLayout | ||||
|       android:id="@+id/upload_progress_toolbar_layout" | ||||
|       android:layout_width="match_parent" | ||||
|       android:layout_height="wrap_content" | ||||
|       android:background="@color/card_light_grey"> | ||||
| 
 | ||||
|       <com.google.android.material.tabs.TabLayout | ||||
|         android:id="@+id/upload_progress_tab_layout" | ||||
|         android:layout_width="match_parent" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:layout_below="@id/toolbar" | ||||
|         android:background="?attr/tabBackground" | ||||
|         app:tabIndicatorColor="?attr/tabIndicatorColor" | ||||
|         app:tabMode="fixed" | ||||
|         app:tabSelectedTextColor="?attr/tabSelectedTextColor" | ||||
|         app:tabTextColor="?attr/tabTextColor" /> | ||||
|     </com.google.android.material.appbar.AppBarLayout> | ||||
| 
 | ||||
|     <fr.free.nrw.commons.explore.ParentViewPager | ||||
|       android:id="@+id/upload_progress_view_pager" | ||||
|       android:layout_width="match_parent" | ||||
|       android:layout_height="match_parent" | ||||
|       android:layout_below="@id/upload_progress_toolbar_layout" | ||||
|       android:background="?attr/mainBackground" /> | ||||
| 
 | ||||
|   </RelativeLayout> | ||||
| 
 | ||||
| </LinearLayout> | ||||
|  | @ -18,36 +18,6 @@ | |||
|       android:layout_marginTop="@dimen/miniscule_margin" | ||||
|       android:layout_margin="@dimen/very_tiny_gap"/> | ||||
| 
 | ||||
|   <LinearLayout | ||||
|     android:id="@+id/limited_connection_enabled_layout" | ||||
|     android:animateLayoutChanges="true" | ||||
|     android:layout_width="match_parent" | ||||
|     android:layout_height="wrap_content" | ||||
|     android:layout_margin="@dimen/miniscule_margin" | ||||
|     android:padding="@dimen/standard_gap" | ||||
|     android:orientation="vertical" | ||||
|     android:clickable="true" | ||||
|     android:focusable="true" | ||||
|     android:background="@color/wikimedia_green"> | ||||
|     <TextView | ||||
|       android:layout_width="wrap_content" | ||||
|       android:layout_height="wrap_content" | ||||
|       android:drawablePadding="5dp" | ||||
|       android:textColor="@android:color/white" | ||||
|       android:layout_marginBottom="@dimen/tiny_gap" | ||||
|       android:textSize="@dimen/subheading_text_size" | ||||
|       android:text="@string/limited_connection_is_on" | ||||
|       app:drawableTint="@color/white" | ||||
|       app:drawableStartCompat="@drawable/ic_baseline_cloud_off_24"/> | ||||
|     <TextView | ||||
|       android:id="@+id/limited_connection_description_text_view" | ||||
|       android:layout_width="wrap_content" | ||||
|       android:layout_height="wrap_content" | ||||
|       android:textColor="@android:color/white" | ||||
|       android:textSize="@dimen/description_text_size" | ||||
|       android:text="@string/limited_connection_explanation"/> | ||||
|   </LinearLayout> | ||||
| 
 | ||||
|   <FrameLayout | ||||
|     android:id="@+id/explore_container" | ||||
|     android:layout_width="match_parent" | ||||
|  |  | |||
							
								
								
									
										33
									
								
								app/src/main/res/layout/fragment_failed_uploads.xml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								app/src/main/res/layout/fragment_failed_uploads.xml
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,33 @@ | |||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|   xmlns:tools="http://schemas.android.com/tools" | ||||
|   android:layout_width="match_parent" | ||||
|   android:layout_height="match_parent" | ||||
|   android:gravity="center" | ||||
|   android:orientation="vertical" | ||||
|   tools:context=".upload.FailedUploadsFragment"> | ||||
| 
 | ||||
|   <TextView | ||||
|     android:id="@+id/nofailedTextView" | ||||
|     android:layout_width="match_parent" | ||||
|     android:layout_height="wrap_content" | ||||
|     android:gravity="center" | ||||
|     android:text="You do not have any failed Uploads!" /> | ||||
| 
 | ||||
|   <LinearLayout | ||||
|     android:id="@+id/failedUplaodsLl" | ||||
|     android:layout_width="match_parent" | ||||
|     android:layout_height="match_parent" | ||||
|     android:layout_marginTop="10dp" | ||||
|     android:orientation="vertical" | ||||
|     android:visibility="gone"> | ||||
| 
 | ||||
|     <androidx.recyclerview.widget.RecyclerView | ||||
|       android:id="@+id/failed_uploads_recycler_view" | ||||
|       android:layout_width="match_parent" | ||||
|       android:layout_height="match_parent" | ||||
|       android:layout_marginHorizontal="10dp" /> | ||||
|   </LinearLayout> | ||||
| 
 | ||||
| </LinearLayout> | ||||
| 
 | ||||
							
								
								
									
										67
									
								
								app/src/main/res/layout/fragment_pending_uploads.xml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								app/src/main/res/layout/fragment_pending_uploads.xml
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,67 @@ | |||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|   xmlns:tools="http://schemas.android.com/tools" | ||||
|   android:layout_width="match_parent" | ||||
|   android:layout_height="match_parent" | ||||
|   android:gravity="center" | ||||
|   android:orientation="vertical" | ||||
|   tools:context=".upload.PendingUploadsFragment"> | ||||
| 
 | ||||
|   <TextView | ||||
|     android:id="@+id/nopendingTextView" | ||||
|     android:layout_width="match_parent" | ||||
|     android:layout_height="wrap_content" | ||||
|     android:gravity="center" | ||||
|     android:text="You do not have any pending Uploads!" | ||||
|     android:visibility="gone" /> | ||||
| 
 | ||||
|   <LinearLayout | ||||
|     android:id="@+id/pendingUplaodsLl" | ||||
|     android:layout_width="match_parent" | ||||
|     android:layout_height="match_parent" | ||||
|     android:orientation="vertical" | ||||
|     android:visibility="visible"> | ||||
| 
 | ||||
|     <LinearLayout | ||||
|       android:layout_width="match_parent" | ||||
|       android:layout_height="wrap_content" | ||||
|       android:layout_margin="10dp" | ||||
|       android:gravity="bottom" | ||||
|       android:orientation="horizontal"> | ||||
| 
 | ||||
|       <TextView | ||||
|         android:layout_width="match_parent" | ||||
|         android:layout_height="match_parent" | ||||
|         android:layout_weight="1" | ||||
|         android:text="Progress:" | ||||
|         android:textSize="22sp" /> | ||||
| 
 | ||||
|       <LinearLayout | ||||
|         android:layout_width="match_parent" | ||||
|         android:layout_height="match_parent" | ||||
|         android:layout_weight="1" | ||||
|         android:gravity="center" | ||||
|         android:orientation="vertical"> | ||||
| 
 | ||||
|         <TextView | ||||
|           android:id="@+id/progress_text_view" | ||||
|           android:layout_width="match_parent" | ||||
|           android:layout_height="wrap_content" | ||||
|           android:gravity="right" | ||||
|           android:text="" | ||||
|           android:textSize="21sp" /> | ||||
| 
 | ||||
|       </LinearLayout> | ||||
| 
 | ||||
|     </LinearLayout> | ||||
| 
 | ||||
|     <androidx.recyclerview.widget.RecyclerView | ||||
|       android:id="@+id/pending_uploads_recycler_view" | ||||
|       android:layout_width="match_parent" | ||||
|       android:layout_height="match_parent" | ||||
|       android:layout_marginHorizontal="10dp" /> | ||||
| 
 | ||||
|   </LinearLayout> | ||||
| 
 | ||||
| 
 | ||||
| </LinearLayout> | ||||
							
								
								
									
										61
									
								
								app/src/main/res/layout/item_failed_upload.xml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								app/src/main/res/layout/item_failed_upload.xml
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,61 @@ | |||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|   android:layout_width="match_parent" | ||||
|   android:layout_height="wrap_content" | ||||
|   xmlns:app="http://schemas.android.com/apk/res-auto" | ||||
|   xmlns:fresco="http://schemas.android.com/tools" | ||||
|   android:paddingBottom="8dp" | ||||
|   android:gravity="center" | ||||
|   android:orientation="horizontal"> | ||||
| 
 | ||||
|   <com.facebook.drawee.view.SimpleDraweeView | ||||
|     android:id="@+id/itemImage" | ||||
|     android:layout_width="50dp" | ||||
|     android:layout_height="50dp" | ||||
|     android:background="?attr/mainBackground" | ||||
|     app:actualImageScaleType="centerCrop" | ||||
|     fresco:placeholderImage="@drawable/ic_image_black_24dp" /> | ||||
| 
 | ||||
|   <LinearLayout | ||||
|     android:layout_width="match_parent" | ||||
|     android:layout_height="wrap_content" | ||||
|     android:paddingHorizontal="6dp" | ||||
|     android:layout_weight="1" | ||||
|     android:gravity="center" | ||||
|     android:orientation="vertical"> | ||||
| 
 | ||||
|     <TextView | ||||
|       android:id="@+id/titleTextView" | ||||
|       android:layout_width="match_parent" | ||||
|       android:layout_height="wrap_content" | ||||
|       android:textSize="24sp"/> | ||||
| 
 | ||||
|     <ProgressBar | ||||
|       android:id="@+id/itemProgress" | ||||
|       style="?android:attr/progressBarStyleHorizontal" | ||||
|       android:layout_width="match_parent" | ||||
|       android:layout_height="wrap_content" /> | ||||
| 
 | ||||
|     <TextView | ||||
|       android:id="@+id/errorTextView" | ||||
|       android:layout_width="match_parent" | ||||
|       android:layout_height="wrap_content" | ||||
|       android:text="Queued" | ||||
|       android:visibility="gone" /> | ||||
| 
 | ||||
|   </LinearLayout> | ||||
| 
 | ||||
|   <ImageView | ||||
|     android:id="@+id/retryButton" | ||||
|     android:layout_width="wrap_content" | ||||
|     android:layout_height="wrap_content" | ||||
|     android:layout_marginEnd="@dimen/dimen_10" | ||||
|     android:src="@drawable/ic_refresh_24dp" /> | ||||
| 
 | ||||
|   <ImageView | ||||
|     android:id="@+id/deleteButton" | ||||
|     android:layout_width="wrap_content" | ||||
|     android:layout_height="wrap_content" | ||||
|     android:src="@drawable/ic_cancel_upload" /> | ||||
| 
 | ||||
| </LinearLayout> | ||||
							
								
								
									
										64
									
								
								app/src/main/res/layout/item_pending_upload.xml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								app/src/main/res/layout/item_pending_upload.xml
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,64 @@ | |||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|   xmlns:app="http://schemas.android.com/apk/res-auto" | ||||
|   xmlns:fresco="http://schemas.android.com/tools" | ||||
|   android:layout_width="match_parent" | ||||
|   android:layout_height="wrap_content" | ||||
|   android:gravity="center" | ||||
|   android:orientation="horizontal" | ||||
|   android:paddingBottom="8dp"> | ||||
| 
 | ||||
|   <com.facebook.drawee.view.SimpleDraweeView | ||||
|     android:id="@+id/itemImage" | ||||
|     android:layout_width="50dp" | ||||
|     android:layout_height="50dp" | ||||
|     android:layout_marginBottom="8dp" | ||||
|     android:background="?attr/mainBackground" | ||||
|     app:actualImageScaleType="centerCrop" | ||||
|     app:layout_constraintBottom_toBottomOf="parent" | ||||
|     app:layout_constraintStart_toStartOf="parent" | ||||
|     app:layout_constraintTop_toTopOf="parent" | ||||
|     fresco:placeholderImage="@drawable/ic_image_black_24dp" /> | ||||
| 
 | ||||
|   <LinearLayout | ||||
|     android:layout_width="0dp" | ||||
|     android:layout_height="wrap_content" | ||||
|     android:layout_weight="1" | ||||
|     android:gravity="center" | ||||
|     android:orientation="vertical" | ||||
|     android:paddingHorizontal="6dp" | ||||
|     app:layout_constraintEnd_toStartOf="@+id/deleteButton" | ||||
|     app:layout_constraintStart_toEndOf="@+id/itemImage"> | ||||
| 
 | ||||
|     <TextView | ||||
|       android:id="@+id/titleTextView" | ||||
|       android:layout_width="match_parent" | ||||
|       android:layout_height="wrap_content" | ||||
|       android:textSize="24sp" /> | ||||
| 
 | ||||
|     <ProgressBar | ||||
|       android:id="@+id/itemProgress" | ||||
|       style="?android:attr/progressBarStyleHorizontal" | ||||
|       android:layout_width="match_parent" | ||||
|       android:layout_height="wrap_content" | ||||
|       android:visibility="visible" /> | ||||
| 
 | ||||
|     <TextView | ||||
|       android:id="@+id/errorTextView" | ||||
|       android:layout_width="match_parent" | ||||
|       android:layout_height="wrap_content" | ||||
|       android:text="Queued" | ||||
|       android:visibility="gone" /> | ||||
| 
 | ||||
|   </LinearLayout> | ||||
| 
 | ||||
|   <ImageView | ||||
|     android:id="@+id/deleteButton" | ||||
|     android:layout_width="wrap_content" | ||||
|     android:layout_height="wrap_content" | ||||
|     android:src="@drawable/ic_cancel_upload" | ||||
|     app:layout_constraintBottom_toBottomOf="parent" | ||||
|     app:layout_constraintEnd_toEndOf="parent" | ||||
|     app:layout_constraintTop_toTopOf="parent" /> | ||||
| 
 | ||||
| </androidx.constraintlayout.widget.ConstraintLayout> | ||||
|  | @ -104,40 +104,6 @@ | |||
|       android:paddingTop="@dimen/standard_gap" | ||||
|       android:visibility="visible"> | ||||
| 
 | ||||
|       <ImageButton | ||||
|         android:id="@+id/pauseResumeButton" | ||||
|         android:layout_width="@dimen/dimen_40" | ||||
|         android:layout_height="@dimen/dimen_40" | ||||
|         android:layout_marginEnd="@dimen/tiny_padding" | ||||
|         android:layout_toStartOf="@id/cancelButton" | ||||
|         android:background="@android:color/transparent" | ||||
|         android:tag="@string/pause" | ||||
|         app:srcCompat="@drawable/pause_icon" /> | ||||
| 
 | ||||
|       <ImageButton | ||||
|         android:id="@+id/cancelButton" | ||||
|         android:layout_width="48dp" | ||||
|         android:layout_height="48dp" | ||||
|         android:layout_marginEnd="@dimen/tiny_padding" | ||||
|         android:layout_toStartOf="@id/retryButton" | ||||
|         android:background="@android:color/transparent" | ||||
|         android:padding="@dimen/activity_margin_horizontal" | ||||
|         android:src="@drawable/ic_cancel_white" | ||||
|         android:tint="?attr/contributionsListTextSecondary" | ||||
|         android:text="@string/menu_cancel_upload" /> | ||||
| 
 | ||||
|       <ImageButton | ||||
|         android:id="@+id/retryButton" | ||||
|         android:layout_width="48dp" | ||||
|         android:layout_height="48dp" | ||||
|         android:layout_marginEnd="@dimen/tiny_padding" | ||||
|         android:layout_toStartOf="@id/wikipediaButton" | ||||
|         android:background="@android:color/transparent" | ||||
|         android:padding="@dimen/activity_margin_horizontal" | ||||
|         android:src="@drawable/ic_retry_white" | ||||
|         android:tint="?attr/contributionsListTextSecondary" | ||||
|         android:text="@string/menu_retry_upload" /> | ||||
| 
 | ||||
|       <ImageButton | ||||
|         android:id="@+id/wikipediaButton" | ||||
|         android:layout_width="48dp" | ||||
|  |  | |||
							
								
								
									
										60
									
								
								app/src/main/res/layout/pending_uploads_icon.xml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								app/src/main/res/layout/pending_uploads_icon.xml
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,60 @@ | |||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     xmlns:app="http://schemas.android.com/apk/res-auto" | ||||
|     android:layout_width="wrap_content" | ||||
|     xmlns:tools="http://schemas.android.com/tools" | ||||
|     android:layout_height="wrap_content" | ||||
|     android:background="@android:color/transparent" | ||||
|     android:clickable="true" | ||||
|     android:focusable="true" | ||||
|     android:gravity="center" | ||||
|     tools:background="@color/black"> | ||||
| 
 | ||||
|     <ImageView | ||||
|       android:id="@+id/pending_uploads_image_view" | ||||
|       android:layout_width="wrap_content" | ||||
|       android:layout_height="wrap_content" | ||||
|       android:visibility="gone" | ||||
|       android:layout_marginEnd="@dimen/activity_margin_horizontal" | ||||
|       android:layout_marginRight="@dimen/activity_margin_horizontal" | ||||
|       android:gravity="center" | ||||
|       app:srcCompat="?attr/upload_icon_drawable" /> | ||||
| 
 | ||||
|     <TextView | ||||
|       android:id="@+id/pending_uploads_count_badge" | ||||
|       android:layout_width="wrap_content" | ||||
|       android:layout_height="wrap_content" | ||||
|       android:layout_alignTop="@id/pending_uploads_image_view" | ||||
|       android:layout_alignEnd="@id/pending_uploads_image_view" | ||||
|       android:layout_alignRight="@id/pending_uploads_image_view" | ||||
|       android:background="@drawable/notification_badge" | ||||
|       android:backgroundTint="@color/button_blue" | ||||
|       android:gravity="center" | ||||
|       android:padding="@dimen/miniscule_margin" | ||||
|       android:textColor="@color/white" | ||||
|       android:textSize="7sp" | ||||
|       android:textStyle="bold" | ||||
|       android:visibility="gone" | ||||
|       tools:text="9+" | ||||
|       tools:visibility="visible" /> | ||||
| 
 | ||||
|     <TextView | ||||
|       android:id="@+id/uploads_error_count_badge" | ||||
|       android:layout_width="wrap_content" | ||||
|       android:layout_height="wrap_content" | ||||
|       android:layout_below="@+id/pending_uploads_count_badge" | ||||
|       android:layout_alignEnd="@id/pending_uploads_image_view" | ||||
|       android:layout_alignRight="@id/pending_uploads_image_view" | ||||
|       android:layout_marginTop="1dp" | ||||
|       android:layout_marginEnd="0dp" | ||||
|       android:layout_marginRight="0dp" | ||||
|       android:background="@drawable/notification_badge" | ||||
|       android:gravity="center" | ||||
|       android:padding="@dimen/miniscule_margin" | ||||
|       android:textColor="?attr/notification_icon_text_color" | ||||
|       android:textSize="7sp" | ||||
|       android:textStyle="bold" | ||||
|       android:visibility="gone" | ||||
|       tools:text="9+" | ||||
|       tools:visibility="visible" /> | ||||
| </RelativeLayout> | ||||
|  | @ -1,16 +1,14 @@ | |||
| <menu xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|   xmlns:app="http://schemas.android.com/apk/res-auto"> | ||||
| 
 | ||||
|     <item android:id="@+id/toggle_limited_connection_mode" | ||||
|       android:title="@string/limited_connection_mode" | ||||
|       app:showAsAction="always" | ||||
|       android:checkable="true" | ||||
|       android:icon="@drawable/ic_baseline_cloud_queue_24" | ||||
|       /> | ||||
|     <item android:id="@+id/notifications" | ||||
|         android:title="@string/notifications" | ||||
|         app:showAsAction="ifRoom|withText" | ||||
|   <item | ||||
|     android:id="@+id/upload_tab" | ||||
|     android:title="Upload" | ||||
|     app:actionLayout="@layout/pending_uploads_icon" | ||||
|     app:showAsAction="ifRoom|withText" /> | ||||
|   <item | ||||
|     android:id="@+id/notifications" | ||||
|     android:menuCategory="secondary" | ||||
|     android:title="@string/notifications" | ||||
|     app:actionLayout="@layout/notification_icon" | ||||
|         /> | ||||
|     app:showAsAction="ifRoom|withText" /> | ||||
| </menu> | ||||
|  |  | |||
							
								
								
									
										37
									
								
								app/src/main/res/menu/menu_uploads.xml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								app/src/main/res/menu/menu_uploads.xml
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,37 @@ | |||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <menu xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|   xmlns:app="http://schemas.android.com/apk/res-auto" | ||||
|   xmlns:tools="http://schemas.android.com/tools" | ||||
|   tools:context=".upload.UploadProgressActivity" | ||||
|   > | ||||
|   <item | ||||
|     android:id="@+id/resume_icon" | ||||
|     android:title="Resume" | ||||
|     android:icon="@drawable/play_icon" | ||||
|     android:orderInCategory="1" | ||||
|     app:showAsAction="ifRoom" | ||||
|     /> | ||||
|   <item | ||||
|     android:id="@+id/pause_icon" | ||||
|     android:title="Pause" | ||||
|     android:icon="@drawable/pause_icon" | ||||
|     android:orderInCategory="1" | ||||
|     app:showAsAction="ifRoom" | ||||
|     /> | ||||
| 
 | ||||
|   <item | ||||
|     android:id="@+id/retry_icon" | ||||
|     android:title="Retry" | ||||
|     android:icon="@drawable/ic_refresh_24dp" | ||||
|     android:orderInCategory="1" | ||||
|     app:showAsAction="ifRoom" | ||||
|     /> | ||||
| 
 | ||||
|   <item | ||||
|     android:id="@+id/cancel_icon" | ||||
|     android:title="Cancel" | ||||
|     android:icon="@drawable/ic_remove" | ||||
|     android:orderInCategory="1" | ||||
|     app:showAsAction="ifRoom" | ||||
|     /> | ||||
| </menu> | ||||
|  | @ -783,5 +783,10 @@ | |||
|   <string name="is_at_a_different_place_please_specify_the_correct_place_below_if_possible_tell_us_the_correct_latitude_longitude">\'%1$s\' ligger et andet sted. Angiv venligst det korrekte sted nedenfor, og skriv om muligt den korrekte bredde- og længdegrad.</string> | ||||
|   <string name="other_problem_or_information_please_explain_below">Andet problem eller anden information (forklar venligst nedenfor).</string> | ||||
|   <string name="feedback_destination_note">Din feedback bliver slået op på følgende wiki-side:  <a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Mobile app/Feedback</a></string> | ||||
|   <string name="are_you_sure_that_you_want_cancel_all_the_uploads">Er du sikker på, at du vil annullere alle uploads?</string> | ||||
|   <string name="cancelling_all_the_uploads">Annullerer alle uploads...</string> | ||||
|   <string name="uploads">Uploads</string> | ||||
|   <string name="pending">Afventer</string> | ||||
|   <string name="failed">Mislykkedes</string> | ||||
|   <string name="could_not_load_place_data">Kunne ikke indlæse steddata</string> | ||||
| </resources> | ||||
|  |  | |||
|  | @ -403,57 +403,57 @@ | |||
|   <string name="notifications">Ενημερώσεις</string> | ||||
|   <string name="read_notifications">Ειδοποιήσεις (ανάγνωση)</string> | ||||
|   <string name="display_nearby_notification">Εμφάνιση ειδοποίησης σε κοντινή απόσταση</string> | ||||
|   <string name="display_nearby_notification_summary">Πατήστε εδώ για να δείτε την πιο κοντινή θέση που χρειάζεται εικόνες</string> | ||||
|   <string name="list_sheet">Λίστα</string> | ||||
|   <string name="storage_permission">Άδεια Αποθήκευσης</string> | ||||
|   <string name="display_nearby_notification_summary">Εμφάνιση ειδοποίησης εντός της εφαρμογής για το πλησιέστερο μέρος που χρειάζεται φωτογραφίες</string> | ||||
|   <string name="list_sheet">Κατάλογος</string> | ||||
|   <string name="storage_permission">Άδεια αποθήκευσης</string> | ||||
|   <string name="write_storage_permission_rationale_for_image_share">Χρειαζόμαστε την άδειά σας για πρόσβαση στον εξωτερικό χώρο αποθήκευσης της συσκευής σας προκειμένου να ανεβάσουμε εικόνες.</string> | ||||
|   <string name="nearby_notification_dismiss_message">Δεν θα δείτε την πιο κοντινή τοποθεσία που χρειάζεται επιπλέον εικόνες. Ωστόσο, μπορείτε να ενεργοποιήσετε ξανά αυτή την ειδοποίηση στις Ρυθμίσεις αν θέλετε.</string> | ||||
|   <string name="nearby_notification_dismiss_message">Δε θα βλέπετε πλέον το πλησιέστερο μέρος που χρειάζεται φωτογραφίες. Ωστόσο, μπορείτε να ενεργοποιήσετε ξανά αυτή την ειδοποίηση στις Ρυθμίσεις, αν το επιθυμείτε.</string> | ||||
|   <string name="step_count">Βήμα %1$d από %2$d: %3$s</string> | ||||
|   <string name="next">Επόμενο</string> | ||||
|   <string name="previous">Προηγούμενο</string> | ||||
|   <string name="upload_title_duplicate">Υπάρχει ήδη αρχείο με όνομα %1$s. Είστε σίγουροι πως θέλετε να προχωρήσετε;\n\nΣημείωση: θα προστεθεί αυτόματα μια διόρθωση όνομα αρχείου.</string> | ||||
|   <string name="map_application_missing">Καμία εφαρμογή χάρτη δεν βρέθηκε στον υπολογιστή. Παρακαλώ εγκαταστήστε εφαρμογή χάρτη για να χρησιμοποιήσετε αυτήν την ιδιότητα.</string> | ||||
|   <string name="title_page_bookmarks_pictures">εικόνες</string> | ||||
|   <string name="upload_title_duplicate">Υπάρχει ήδη αρχείο με το όνομα %1$s. Είστε σίγουροι πως θέλετε να προχωρήσετε;\n\nΣημείωση: Ένα κατάλληλο επίθημα θα προστεθεί αυτόματα στο όνομα του αρχείου.</string> | ||||
|   <string name="map_application_missing">Δε βρέθηκε καμία συμβατή εφαρμογή χάρτη στη συσκευή σας. Εγκαταστήστε μια εφαρμογή χάρτη για να χρησιμοποιήσετε αυτήν τη δυνατότητα.</string> | ||||
|   <string name="title_page_bookmarks_pictures">Φωτογραφίες</string> | ||||
|   <string name="title_page_bookmarks_locations">Τοποθεσίες</string> | ||||
|   <string name="menu_bookmark">Προσθήκη/Κατάργηση σε σελιδοδείκτες</string> | ||||
|   <string name="provider_bookmarks">Σελιδοδείκτες</string> | ||||
|   <string name="bookmark_empty">Δεν έχετε προσθέσει σελιδοδείκτες</string> | ||||
|   <string name="provider_bookmarks_location">Σελιδοδείκτες</string> | ||||
|   <string name="log_collection_started">Η συλλογή αρχείων καταγραφής ξεκίνησε. ΕΠΑΝΑΚΙΝΗΣΤΕ την εφαρμογή, εκτελέστε την ενέργεια που θέλετε να καταγράψετε και, στη συνέχεια, πατήστε ξανά \"Αποστολή αρχείου καταγραφής\"</string> | ||||
|   <string name="deletion_reason_uploaded_by_mistake">Το ανέβασα κατά λάθος</string> | ||||
|   <string name="deletion_reason_publicly_visible">Δεν ήξερα ότι θα δημοσιευόταν</string> | ||||
|   <string name="deletion_reason_bad_for_my_privacy">Κατάλαβα πως δεν προστατεύονται τα ατομικά μου στοιχεία</string> | ||||
|   <string name="deletion_reason_no_longer_want_public">Άλλαξα γνώμη, δεν θέλω να προβάλλεται πλέον δημόσια</string> | ||||
|   <string name="deletion_reason_not_interesting">Λυπάμαι αυτή η εικόνα δεν έχει ενδιαφέρον για εγκυκλοπαίδεια</string> | ||||
|   <string name="uploaded_by_myself">Ανέβηκε από εμένα στο %1$s, χρησιμοποιήθηκε σε %2$d άρθρο(α)</string> | ||||
|   <string name="no_uploads">Καλώς ήρθατε στα Commons!\n\nΑνεβάστε τα πρώτα σας πολυμέσα πατώντας το κουμπί προσθήκης.</string> | ||||
|   <string name="log_collection_started">Η συλλογή αρχείων καταγραφής ξεκίνησε. ΕΠΑΝΕΚΚΙΝΗΣΤΕ την εφαρμογή, εκτελέστε την ενέργεια που επιθυμείτε να καταγράψετε και, στη συνέχεια, πατήστε ξανά «Αποστολή αρχείου καταγραφής»</string> | ||||
|   <string name="deletion_reason_uploaded_by_mistake">Το μεταφόρτωσα κατά λάθος</string> | ||||
|   <string name="deletion_reason_publicly_visible">Δεν ήξερα ότι θα ήταν δημόσια ορατό</string> | ||||
|   <string name="deletion_reason_bad_for_my_privacy">Συνειδητοποίησα ότι είναι κακό για την ιδιωτικότητά μου</string> | ||||
|   <string name="deletion_reason_no_longer_want_public">Άλλαξα γνώμη, δε θέλω να προβάλλεται πλέον δημόσια</string> | ||||
|   <string name="deletion_reason_not_interesting">Συγγνώμη, αυτή η φωτογραφία δεν είναι ενδιαφέρουσα για μια εγκυκλοπαίδεια</string> | ||||
|   <string name="uploaded_by_myself">Ανέβηκε από εμένα στο %1$s, χρησιμοποιήθηκε σε %2$d άρθρο/α</string> | ||||
|   <string name="no_uploads">Καλώς ήρθατε στα Commons!\n\nΑνεβάστε τα πρώτα σας πολυμέσα πατώντας το κουμπί της προσθήκης.</string> | ||||
|   <string name="no_categories_selected">Δεν επιλέχθηκαν κατηγορίες</string> | ||||
|   <string name="no_categories_selected_warning_desc">Εικόνες χωρίς κατηγορίες χρησιμοποιούνται σπάνια. Θέλετε πράγματι να συνεχίσετε δίχως να επιλέξετε κατηγορίες?</string> | ||||
|   <string name="no_depictions_selected">Δεν έχουν επιλεγεί αποτυπώσεις</string> | ||||
|   <string name="no_depictions_selected_warning_desc">Οι εικόνες με απεικονίσεις βρίσκονται πιο εύκολα και πιο πιθανό να χρησιμοποιηθούν. Είστε βέβαιοι ότι θέλετε να συνεχίσετε χωρίς να επιλέξετε απεικονίσεις;</string> | ||||
|   <string name="no_categories_selected_warning_desc">Οι εικόνες χωρίς κατηγορίες χρησιμοποιούνται σπάνια. Θέλετε πράγματι να συνεχίσετε δίχως να επιλέξετε κατηγορίες;</string> | ||||
|   <string name="no_depictions_selected">Δεν έχουν επιλεγεί απεικονίσεις</string> | ||||
|   <string name="no_depictions_selected_warning_desc">Οι εικόνες με απεικονίσεις είναι πιο εύκολα ανιχνεύσιμες και πιο πιθανό να χρησιμοποιηθούν. Θέλετε σίγουρα να συνεχίσετε χωρίς να επιλέξετε απεικονίσεις;</string> | ||||
|   <string name="back_button_warning">Ακύρωση Μεταφόρτωσης</string> | ||||
|   <string name="back_button_warning_desc">Η χρήση του κουμπιού \"πίσω\" θα ακυρώσει αυτήν τη μεταφόρτωση και θα χάσετε την πρόοδό σας</string> | ||||
|   <string name="back_button_warning_desc">Χρησιμοποιώντας το κουμπί επιστροφής θα ακυρώσετε αυτή τη μεταφόρτωση και θα χάσετε την πρόοδό σας</string> | ||||
|   <string name="back_button_continue">Συνέχιση Μεταφόρτωσης</string> | ||||
|   <string name="upload_flow_all_images_in_set">(Για όλες τις εικόνες στο σετ)</string> | ||||
|   <string name="upload_flow_all_images_in_set">(Για όλες τις εικόνες στο σύνολο)</string> | ||||
|   <string name="search_this_area">Αναζήτηση στην περιοχή</string> | ||||
|   <string name="nearby_card_permission_title">Αίτημα Άδειας</string> | ||||
|   <string name="nearby_card_permission_explanation">Θα θέλατε να χρησιμοποιήσουμε την τρέχουσα τοποθεσία σας για να εμφανίσουμε το πλησιέστερο μέρος που χρειάζεται φωτογραφίες;</string> | ||||
|   <string name="unable_to_display_nearest_place">Δεν είναι δυνατή η εμφάνιση του πλησιέστερου μέρους που χρειάζεται φωτογραφίες χωρίς δικαιώματα τοποθεσίας</string> | ||||
|   <string name="never_ask_again">Μην το ρωτήσετε ξανά αυτό</string> | ||||
|   <string name="never_ask_again">Μη με ξαναρωτήσετε</string> | ||||
|   <string name="display_location_permission_title">Ζητήστε άδεια τοποθεσίας</string> | ||||
|   <string name="display_location_permission_explanation">Ζητήστε άδεια τοποθεσίας όταν χρειάζεται για τη λειτουργία προβολής κοντινής κάρτας ειδοποιήσεων.</string> | ||||
|   <string name="achievements_fetch_failed">Κάτι πήγε στραβά. Δεν μπορέσαμε να ανακτήσουμε επιτεύγματα</string> | ||||
|   <string name="achievements_fetch_failed_ultimate_achievement">Έχετε κάνει τόσες πολλές συνεισφορές που δεν μπορεί να αντεπεξέλθει το σύστημα υπολογισμού των επιτευγμάτων μας. Αυτό είναι το απόλυτο επίτευγμα.</string> | ||||
|   <string name="ends_on">Τελειώνει σε:</string> | ||||
|   <string name="display_campaigns">Προβολή καμπανιών</string> | ||||
|   <string name="display_campaigns_explanation">Δείτε τις τρέχουσες καμπάνιες</string> | ||||
|   <string name="achievements_fetch_failed_ultimate_achievement">Έχετε κάνει τόσες πολλές συνεισφορές που δεν μπορεί να αντεπεξέλθει το σύστημα υπολογισμού επιτευγμάτων μας. Αυτό είναι το απόλυτο επίτευγμα.</string> | ||||
|   <string name="ends_on">Λήγει στις:</string> | ||||
|   <string name="display_campaigns">Προβολή εκστρατειών</string> | ||||
|   <string name="display_campaigns_explanation">Δείτε τις τρέχουσες εκστρατείες</string> | ||||
|   <string name="in_app_camera_location_access_explanation">Επιτρέψτε στην εφαρμογή να ανακτήσει τοποθεσία σε περίπτωση που η κάμερα δεν την καταγράψει. Ορισμένες κάμερες συσκευών δεν καταγράφουν τοποθεσία. Σε τέτοιες περιπτώσεις, το να αφήσετε την εφαρμογή να ανακτήσει και να επισυνάψει τοποθεσία καθιστά τη συνεισφορά σας πιο χρήσιμη. Μπορείτε να το αλλάξετε ανά πάσα στιγμή από τις Ρυθμίσεις</string> | ||||
|   <string name="option_allow">Επιτρέψτε</string> | ||||
|   <string name="option_allow">Αποδοχή</string> | ||||
|   <string name="option_dismiss">Απόρριψη</string> | ||||
|   <string name="in_app_camera_needs_location">Ενεργοποιήστε την πρόσβαση τοποθεσίας από τις Ρυθμίσεις και δοκιμάστε ξανά. \n\nΣημείωση: Η μεταφόρτωση ενδέχεται να μην έχει τοποθεσία, εάν η εφαρμογή δεν μπορεί να ανακτήσει την τοποθεσία από τη συσκευή σε σύντομο χρονικό διάστημα.</string> | ||||
|   <string name="in_app_camera_needs_location">Ενεργοποιήστε την πρόσβαση τοποθεσίας από τις Ρυθμίσεις και δοκιμάστε ξανά.\n\nΣημείωση: Η μεταφόρτωση ενδέχεται να μην έχει τοποθεσία, εάν η εφαρμογή δεν μπορεί να ανακτήσει την τοποθεσία από τη συσκευή σε σύντομο χρονικό διάστημα.</string> | ||||
|   <string name="in_app_camera_location_permission_rationale">Η κάμερα εντός εφαρμογής χρειάζεται άδεια τοποθεσίας για να την επισυνάψει στις εικόνες σας σε περίπτωση που η τοποθεσία δεν είναι διαθέσιμη στο EXIF. Επιτρέψτε στην εφαρμογή να αποκτήσει πρόσβαση στην τοποθεσία σας και δοκιμάστε ξανά.\n\nΣημείωση: Η μεταφόρτωση ενδέχεται να μην έχει τοποθεσία εάν η εφαρμογή δεν μπορεί να ανακτήσει την τοποθεσία από τη συσκευή σε σύντομο χρονικό διάστημα.</string> | ||||
|   <string name="in_app_camera_location_permission_denied">Η εφαρμογή δεν θα καταγράψει την τοποθεσία μαζί με τις φωτογραφίες λόγω έλλειψης άδειας τοποθεσίας</string> | ||||
|   <string name="in_app_camera_location_unavailable">Η εφαρμογή δεν θα καταγράψει την τοποθεσία μαζί με τις φωτογραφίες καθώς το GPS είναι απενεργοποιημένο</string> | ||||
|   <string name="in_app_camera_location_permission_denied">Η εφαρμογή δε θα καταγράψει την τοποθεσία μαζί με τις φωτογραφίες λόγω έλλειψης άδειας τοποθεσίας</string> | ||||
|   <string name="in_app_camera_location_unavailable">Η εφαρμογή δε θα καταγράψει την τοποθεσία μαζί με τις φωτογραφίες καθώς το GPS είναι απενεργοποιημένο</string> | ||||
|   <string name="open_document_photo_picker_title">Χρησιμοποιήστε εργαλείο επιλογής φωτογραφιών βάσει εγγράφων</string> | ||||
|   <string name="open_document_photo_picker_explanation">Το νέο εργαλείο επιλογής φωτογραφιών Android κινδυνεύει να χάσει τις πληροφορίες τοποθεσίας. Ενεργοποιήστε εάν φαίνεται ότι το χρησιμοποιείτε.</string> | ||||
|   <string name="location_loss_warning">Παρακαλώ σιγουρευτείτε ότι αύτος ο κανούριος επιλογέας Android δεν αφαιρεί την τοποθεσία από τις εικόνες.\n\nΠατήστε στο \'Διαβάστε περισσότερα\' για περισσότερες πληροφορίες.</string> | ||||
|  | @ -797,5 +797,10 @@ | |||
|   <string name="is_at_a_different_place_please_specify_the_correct_place_below_if_possible_tell_us_the_correct_latitude_longitude">Το \'%1$s\' βρίσκεται σε διαφορετική θέση. Παρακαλούμε προσδιορίστε τη σωστή θέση παρακαλώ, και αν είναι εφικτό, γράψτε το σωστό γεωγραφικό πλάτος και μήκος.</string> | ||||
|   <string name="other_problem_or_information_please_explain_below">Άλλο πρόβλημα ή πληροφορίες (παρακαλούμε εξηγήστε παρακάτω).</string> | ||||
|   <string name="feedback_destination_note">Τα σχόλιά σας δημοσιεύονται στην ακόλουθη σελίδα wiki: <a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Εφαρμογή για κινητά/Σχόλια</a></string> | ||||
|   <string name="are_you_sure_that_you_want_cancel_all_the_uploads">Είστε βέβαιοι ότι θέλετε να ακυρώσετε όλες τις μεταφορτώσεις;</string> | ||||
|   <string name="cancelling_all_the_uploads">Ακύρωση όλων των μεταφορτώσεων...</string> | ||||
|   <string name="uploads">Μεταφορτώσεις</string> | ||||
|   <string name="pending">Σε εκκρεμότητα</string> | ||||
|   <string name="failed">Απέτυχε</string> | ||||
|   <string name="could_not_load_place_data">Δεν ήταν δυνατή η φόρτωση δεδομένων της θέσης</string> | ||||
| </resources> | ||||
|  |  | |||
|  | @ -32,6 +32,7 @@ | |||
| * Juanman | ||||
| * Keneth Urrutia | ||||
| * Ktranz | ||||
| * Laquin | ||||
| * Luisangelrg | ||||
| * Macofe | ||||
| * Madamebiblio | ||||
|  | @ -788,6 +789,9 @@ | |||
|   <string name="storage_permissions_denied">Se denegaron los permisos de almacenamiento</string> | ||||
|   <string name="unable_to_share_upload_item">No se puede compartir este elemento</string> | ||||
|   <string name="permissions_are_required_for_functionality">Se requieren permisos para la funcionalidad</string> | ||||
|   <string name="learn_how_to_write_a_useful_description">Aprenda a escribir una descripción útil</string> | ||||
|   <string name="learn_how_to_write_a_useful_caption">Aprenda a escribir una leyenda útil</string> | ||||
|   <string name="see_your_achievements">Ver sus logros</string> | ||||
|   <string name="edit_image">Editar Imagen</string> | ||||
|   <string name="edit_location">Editar Ubicación</string> | ||||
|   <string name="location_updated">¡Ubicación actualizada!</string> | ||||
|  | @ -795,6 +799,8 @@ | |||
|   <string name="remove_location_warning_title">Eliminar el aviso de ubicación</string> | ||||
|   <string name="remove_location_warning_desc">La ubicación hace que las imágenes sean más útiles y accesibles. ¿De verdad quieres eliminar la ubicación de esta foto?</string> | ||||
|   <string name="location_removed">¡Ubicación eliminada!</string> | ||||
|   <string name="send_thanks_to_author">Agradecer al autor</string> | ||||
|   <string name="error_sending_thanks">Error al enviar gracias al autor.</string> | ||||
|   <string name="invalid_login_message">Su sesión ha caducado. Inicie sesión de nuevo.</string> | ||||
|   <string name="no_application_available_to_open_gpx_files">No hay ninguna aplicación disponible para abrir archivos GPX</string> | ||||
|   <string name="file_saved_successfully">Guardado correctamente</string> | ||||
|  | @ -810,4 +816,12 @@ | |||
|   </plurals> | ||||
|   <string name="multiple_files_depiction">Recuerde que todas las imágenes en una carga múltiple tienen la misma categoría y representación. Si las imágenes no comparten representación y categoría, haga varias cargas por separado.</string> | ||||
|   <string name="multiple_files_depiction_header">Nota sobre cargas múltiples</string> | ||||
|   <string name="nearby_wikitalk">Informar a Wikidata sobre un problema relacionado con este elemento</string> | ||||
|   <string name="please_enter_some_comments">Por favor, escriba algunos comentarios.</string> | ||||
|   <string name="talk">Discusión</string> | ||||
|   <string name="write_something_about_the_item">Escriba algo sobre el elemento \'%1$s\'. Será visible públicamente.</string> | ||||
|   <string name="cancelling_all_the_uploads">Cancelando todas las subidas...</string> | ||||
|   <string name="uploads">Subidas</string> | ||||
|   <string name="pending">Pendiente</string> | ||||
|   <string name="failed">Falló</string> | ||||
| </resources> | ||||
|  |  | |||
|  | @ -7,6 +7,7 @@ | |||
| * Fitoschido | ||||
| * Iñaki LL | ||||
| * Joseba | ||||
| * Laquin | ||||
| * Mikel Ibaiba | ||||
| * Sator | ||||
| * Subi | ||||
|  | @ -24,6 +25,11 @@ | |||
|   <string name="add_new_contribution">Ekarpen berria gehitu</string> | ||||
|   <string name="add_contribution_from_camera">Gehitu ekarpena kamaratik</string> | ||||
|   <string name="add_contribution_from_photos">Gehitu ekarpena argazkietatik</string> | ||||
|   <string name="add_contribution_from_contributions_gallery">Gehitu ekarpena aurreko ekarpen-galeriatik</string> | ||||
|   <string name="show_captions">Irudi-oineko testuak</string> | ||||
|   <string name="row_item_language_description">Hizkuntzaren deskribapena</string> | ||||
|   <string name="row_item_caption">Irudi-oineko testua</string> | ||||
|   <string name="nearby_row_image">Irudia</string> | ||||
|   <string name="appwidget_img">Eguneko argazkia</string> | ||||
|   <plurals name="uploads_pending_notification_indicator"> | ||||
|     <item quantity="one">Fitxategi %1$d kargatzen</item> | ||||
|  | @ -67,6 +73,7 @@ | |||
|   <string name="signup">Eman izena</string> | ||||
|   <string name="logging_in_title">Saioa hasten</string> | ||||
|   <string name="logging_in_message">Mesedez itxaron…</string> | ||||
|   <string name="updating_caption_message">Itxaron mesedez…</string> | ||||
|   <string name="login_success">Sarrera arrakastatsua!</string> | ||||
|   <string name="login_failed">Saio hasieran akatsa!</string> | ||||
|   <string name="upload_failed">Fitxategia ez da aurkitu. Mesedez saiatu beste batekin.</string> | ||||
|  | @ -79,6 +86,7 @@ | |||
|   <string name="upload_progress_notification_title_finishing">%1$s igotzen bukatzen</string> | ||||
|   <string name="upload_failed_notification_title">%1$s igotzean akatsa</string> | ||||
|   <string name="upload_failed_notification_subtitle">Ukitu ikusteko</string> | ||||
|   <string name="upload_paused_notification_subtitle">Ukitu ikusteko</string> | ||||
|   <string name="title_activity_contributions">Nire azken igoerak</string> | ||||
|   <string name="contribution_state_queued">Itxoite-zerrendan</string> | ||||
|   <string name="contribution_state_failed">Hutseginda</string> | ||||
|  | @ -97,7 +105,7 @@ | |||
|   <string name="login_failed_throttled">Sartzeko saiakera txar gehiegi. Mesedez saiatu zaitez minutu batzuk barru.</string> | ||||
|   <string name="login_failed_blocked">Barka, baina erabiltzaile hau blokeatuta dago Commonsen</string> | ||||
|   <string name="login_failed_2fa_needed">Zure bi faktoreko autentifikazio kodea eman behar duzu.</string> | ||||
|   <string name="login_failed_generic" fuzzy="true">Saio hasieran akatsa</string> | ||||
|   <string name="login_failed_generic">Saio hasieran akatsa</string> | ||||
|   <string name="share_upload_button">Igo</string> | ||||
|   <string name="multiple_share_base_title">Izena eman bilduma honi</string> | ||||
|   <string name="provider_modifications">Aldaketak</string> | ||||
|  | @ -114,6 +122,7 @@ | |||
|   <string name="title_activity_signup">Eman izena</string> | ||||
|   <string name="title_activity_featured_images">Nabarmendutako irudiak</string> | ||||
|   <string name="title_activity_category_details">Kategoria</string> | ||||
|   <string name="title_activity_review">Parekoen Ebaluazioa</string> | ||||
|   <string name="menu_about">Honi buruz</string> | ||||
|   <string name="about_license">Wikimedia Commons iturri-irekiko aplikazioa da Wikimedia komunitateko bolondresek sortu eta mantendutakoa. Wikimedia Fundazioa ez dago aplikazioaren sorreran, garapenean, edota mantenuan ibili.</string> | ||||
|   <string name="about_improve"><a href=\"%1$s\">GitHub-eko gai</a> berria sortu errore eta iradokizunen berri emateko.</string> | ||||
|  | @ -150,6 +159,7 @@ | |||
|   <string name="tutorial_3_text">Mesedez EZ igo:</string> | ||||
|   <string name="tutorial_3_subtext_1">Autorretratuak edo zure lagunen argazkiak</string> | ||||
|   <string name="tutorial_3_subtext_2">Internetetik jaitsitako irudiak</string> | ||||
|   <string name="tutorial_3_subtext_3">Aplikazio jabedunen pantaila-irudiak</string> | ||||
|   <string name="tutorial_4_text">Igoera adibidea:</string> | ||||
|   <string name="tutorial_4_subtext_1">Izenburua: Sydney Opera House</string> | ||||
|   <string name="tutorial_4_subtext_2">Deskribapena: Sydney Opera House badiaren beste aldetik ikusita</string> | ||||
|  | @ -206,6 +216,7 @@ | |||
|   <string name="navigation_item_about">Honi buruz</string> | ||||
|   <string name="navigation_item_settings">Ezarpenak</string> | ||||
|   <string name="navigation_item_feedback">Feedback</string> | ||||
|   <string name="navigation_item_feedback_github">Github-en bidez berrelikatu</string> | ||||
|   <string name="navigation_item_logout">Saioa itxi</string> | ||||
|   <string name="navigation_item_info">Tutoriala</string> | ||||
|   <string name="navigation_item_notification">Jakinarazpenak</string> | ||||
|  | @ -215,9 +226,12 @@ | |||
|   <string name="nearby_info_menu_wikidata_article">Wikidata itema</string> | ||||
|   <string name="nearby_info_menu_wikipedia_article">Wikipediako artikulua</string> | ||||
|   <string name="description_info">Mesedez, deskribatu multimedia elementua ahal duzun gehien: non hartu zen? zer erakusten du? zein da bere testuingurua? Mesedez, objektuak eta pertsonak deskribatu. Eman asmatzeko erraza ez den informazioa, adibidez, paisaia bat izatekotan, eguneko zein ordutan hartu den. Multimediak zerbait berezia erakusten badu, mesedez azaldu zerk egiten duen berezia.</string> | ||||
|   <string name="upload_problem_exist">Irudi honen arazo potentzialak:</string> | ||||
|   <string name="upload_problem_image_dark">Irudia ilunegia da.</string> | ||||
|   <string name="upload_problem_image_blurry">Argazkia lausoa da.</string> | ||||
|   <string name="upload_problem_image_duplicate">Irudia Commonsen badago.</string> | ||||
|   <string name="upload_problem_different_geolocation">Irudi hau beste leku batean hartu da.</string> | ||||
|   <string name="upload_problem_do_you_continue">Oraindik igo nahi al duzu argazki hau?</string> | ||||
|   <string name="upload_connection_error_alert_title">Konektatzeko Errorea</string> | ||||
|   <string name="upload_problem_image">Arazoak aurkitu dira irudian</string> | ||||
|   <string name="use_external_storage">Irudiak aplikazioan gorde</string> | ||||
|  | @ -273,6 +287,7 @@ | |||
|   <string name="search_tab_title_depictions">Elementuak</string> | ||||
|   <string name="explore_tab_title_featured">Nabarmendua</string> | ||||
|   <string name="explore_tab_title_mobile">Mugikorretik igota</string> | ||||
|   <string name="explore_tab_title_map">Mapa</string> | ||||
|   <string name="successful_wikidata_edit">Irudia gehitu da %1$s-(e)ra Wikidatan!</string> | ||||
|   <string name="wikidata_edit_failure">Ezin izan da dagokion Wikidata entitatea eguneratu!</string> | ||||
|   <string name="menu_set_wallpaper">Horma-paper gisa ezarri</string> | ||||
|  | @ -328,6 +343,7 @@ | |||
|   <string name="provider_bookmarks">Lastermarkak</string> | ||||
|   <string name="provider_bookmarks_location">Lastermarkak</string> | ||||
|   <string name="search_this_area">Leku honetan bilatu</string> | ||||
|   <string name="achievements_fetch_failed_ultimate_achievement">Hainbeste ekarpen egin dituzu, non gure lorpenetarako kalkulu-sistema ez den iristen. Hau da lorpen handiena.</string> | ||||
|   <string name="nominate_for_deletion_done">Egina</string> | ||||
|   <string name="review_thanks_yes_button_text">Hurrengo orria</string> | ||||
|   <string name="review_thanks_no_button_text">Bai, zergatik ez</string> | ||||
|  | @ -344,6 +360,8 @@ | |||
|   <string name="delete_helper_ask_reason_copyright_press_photo">Prentsarako argazkia</string> | ||||
|   <string name="delete_helper_ask_reason_copyright_logo">Logo</string> | ||||
|   <string name="category_edit_helper_show_edit_title_success">Arrakasta</string> | ||||
|   <string name="you_have_no_achievements_yet">Oraindik ez duzu ekarpenik egin</string> | ||||
|   <string name="no_achievements_yet">%s(r)ek oraindik ez du ekarpenik egin</string> | ||||
|   <string name="title_app_shortcut_setting">Hobespenak</string> | ||||
|   <string name="theme_dark_name">Iluna</string> | ||||
|   <string name="theme_light_name">Argia</string> | ||||
|  | @ -358,4 +376,5 @@ | |||
|   <string name="leaderboard_column_count">Zenbaketa</string> | ||||
|   <string name="leaderboard_upload">Igo</string> | ||||
|   <string name="leaderboard_nearby">Hurbilekoak</string> | ||||
|   <string name="contributions_of_user">Erabiltzailearen ekarpenak: %s</string> | ||||
| </resources> | ||||
|  |  | |||
|  | @ -10,13 +10,27 @@ | |||
| * Mguix | ||||
| * Toliño | ||||
| * Vivaelcelta | ||||
| * Xosecalvo | ||||
| --> | ||||
| <resources> | ||||
|   <string name="commons_facebook">Páxina de Commons en Facebook</string> | ||||
|   <string name="commons_github">Código fonte de Commons en Github</string> | ||||
|   <string name="commons_logo">Logo de Commons</string> | ||||
|   <string name="commons_website">Sitio web de Commons</string> | ||||
|   <string name="exit_location_picker">Saír do selector de localización</string> | ||||
|   <string name="submit">Enviar</string> | ||||
|   <string name="add_another_description">Engadir outra descrición</string> | ||||
|   <string name="add_new_contribution">Engadir unha nova achega</string> | ||||
|   <string name="add_contribution_from_camera">Engadir achega desde cámara</string> | ||||
|   <string name="add_contribution_from_photos">Engadir achega desde Photos</string> | ||||
|   <string name="add_contribution_from_contributions_gallery">Engadir achega desde galería de achegas previas</string> | ||||
|   <string name="show_captions">Lendas</string> | ||||
|   <string name="row_item_language_description">Descrición da lingua</string> | ||||
|   <string name="row_item_caption">Lenda</string> | ||||
|   <string name="show_captions_description">Descrición</string> | ||||
|   <string name="nearby_row_image">Imaxe</string> | ||||
|   <string name="nearby_all">Todo</string> | ||||
|   <string name="nearby_filter_search">Vista de busca</string> | ||||
|   <string name="appwidget_img">Imaxe do día</string> | ||||
|   <plurals name="uploads_pending_notification_indicator"> | ||||
|     <item quantity="one">Cargando %1$d ficheiro</item> | ||||
|  | @ -55,6 +69,7 @@ | |||
|   <string name="bullet">•</string> | ||||
|   <string name="menu_settings">Configuracións</string> | ||||
|   <string name="intent_share_upload_label">Cargar en Commons</string> | ||||
|   <string name="upload_in_progress">Envío en curso</string> | ||||
|   <string name="username">Nome de usuario</string> | ||||
|   <string name="password">Contrasinal</string> | ||||
|   <string name="login_credential">Acceda á súa conta de Commons Beta</string> | ||||
|  | @ -63,18 +78,25 @@ | |||
|   <string name="signup">Rexistrarse</string> | ||||
|   <string name="logging_in_title">Accedendo ao sistema</string> | ||||
|   <string name="logging_in_message">Por favor, agarde…</string> | ||||
|   <string name="login_success" fuzzy="true">Accedeu correctamente!</string> | ||||
|   <string name="login_failed" fuzzy="true">Erro durante o inición de sesión!</string> | ||||
|   <string name="updating_caption_title">A actualizar lendas e descricións</string> | ||||
|   <string name="updating_caption_message">Agarde un chisco…</string> | ||||
|   <string name="login_success">Accedeu correctamente!</string> | ||||
|   <string name="login_failed">Erro durante o inició de sesión!</string> | ||||
|   <string name="upload_failed">Ficheiro non atopado. Por favor, probe con outro.</string> | ||||
|   <string name="authentication_failed" fuzzy="true">Erro de autenticación, por favor inicia unha nova sesión</string> | ||||
|   <string name="retry_limit_reached">Alcanzouse o límite máximo de reintentos! Cancele o envío e ténteo de novo</string> | ||||
|   <string name="unrestricted_battery_mode">Desactivar a optimización da batería?</string> | ||||
|   <string name="authentication_failed">Fallou a autenticación. Inicie sesión de novo.</string> | ||||
|   <string name="uploading_started">A carga comezou!</string> | ||||
|   <string name="uploading_queued">Envío en cola (modo de conexión limitado activado)</string> | ||||
|   <string name="upload_completed_notification_title">Cargouse \"%1$s\"!</string> | ||||
|   <string name="upload_completed_notification_text">Prema para ollar a súa carga</string> | ||||
|   <string name="upload_progress_notification_title_start" fuzzy="true">Comezando a carga de \"%1$s\"</string> | ||||
|   <string name="upload_progress_notification_title_start">A enviar ficheiro: %s</string> | ||||
|   <string name="upload_progress_notification_title_in_progress">Cargando \"%1$s\"</string> | ||||
|   <string name="upload_progress_notification_title_finishing">Rematando a carga de \"%1$s\"</string> | ||||
|   <string name="upload_failed_notification_title" fuzzy="true">Erro ao cargar \"%1$s\"</string> | ||||
|   <string name="upload_failed_notification_title">Produciuse un erro ao enviar %1$s</string> | ||||
|   <string name="upload_paused_notification_title">Deteuse o envío de %1$s</string> | ||||
|   <string name="upload_failed_notification_subtitle">Prema para amosalo</string> | ||||
|   <string name="upload_paused_notification_subtitle">Toque para ver</string> | ||||
|   <string name="title_activity_contributions">As miñas subas recentes</string> | ||||
|   <string name="contribution_state_queued">Na cola</string> | ||||
|   <string name="contribution_state_failed">Erróneo</string> | ||||
|  | @ -85,13 +107,15 @@ | |||
|   <string name="menu_nearby">Preto</string> | ||||
|   <string name="provider_contributions">As miñas subidas</string> | ||||
|   <string name="menu_share">Compartir</string> | ||||
|   <string name="menu_view_file_page">Ver a páxina do ficheiro</string> | ||||
|   <string name="share_title_hint">Lenda (Obrigatoria)</string> | ||||
|   <string name="share_description_hint">Descrición</string> | ||||
|   <string name="login_failed_network" fuzzy="true">Erro ao acceder ao sistema: Fallou a rede</string> | ||||
|   <string name="share_caption_hint">Lenda</string> | ||||
|   <string name="login_failed_network">Non foi posíbel acceder ao sistema - fallou a rede</string> | ||||
|   <string name="login_failed_throttled">Demasiados intentos incorrectos. Inténteo de novo nuns minutos.</string> | ||||
|   <string name="login_failed_blocked">Sentímolo, este usuario está bloqueado en Commons</string> | ||||
|   <string name="login_failed_2fa_needed">Debe proporcionar o seu código de autenticación de dous factores.</string> | ||||
|   <string name="login_failed_generic" fuzzy="true">Erro durante o inición de sesión</string> | ||||
|   <string name="login_failed_generic">Fallou o inicio de sesión</string> | ||||
|   <string name="share_upload_button">Subir</string> | ||||
|   <string name="multiple_share_base_title">Dea un nome a este conxunto</string> | ||||
|   <string name="provider_modifications">Modificacións</string> | ||||
|  | @ -102,11 +126,13 @@ | |||
|   <string name="display_list_button">Lista</string> | ||||
|   <string name="contributions_subtitle_zero">(Aínda non hai subas)</string> | ||||
|   <string name="categories_not_found">Non se atopou ningunha categoría que coincidise con \"%1$s\"</string> | ||||
|   <string name="depictions_not_found">Non se atopou ningún elemento de Wikidata que coincida con %1$s</string> | ||||
|   <string name="categories_skip_explanation">Engada categorías para facer máis accesibles as súas imaxes na Wikimedia Commons.\nComece a escribir para engadir categorías.</string> | ||||
|   <string name="categories_activity_title">Categorías</string> | ||||
|   <string name="title_activity_settings">Configuracións</string> | ||||
|   <string name="title_activity_signup">Rexistrarse</string> | ||||
|   <string name="title_activity_featured_images">Imaxes destacadas</string> | ||||
|   <string name="title_activity_custom_selector">Selector personalizado</string> | ||||
|   <string name="title_activity_category_details">Categoría</string> | ||||
|   <string name="title_activity_review">Revisión por pares</string> | ||||
|   <string name="menu_about">Acerca de</string> | ||||
|  | @ -160,6 +186,7 @@ | |||
|   <string name="detail_panel_cats_label">Categorías</string> | ||||
|   <string name="detail_panel_cats_loading">Cargando…</string> | ||||
|   <string name="detail_panel_cats_none">Ningunha seleccionada</string> | ||||
|   <string name="detail_caption_empty">Sen lenda</string> | ||||
|   <string name="detail_description_empty">Sen descrición</string> | ||||
|   <string name="detail_discussion_empty">Sen conversas</string> | ||||
|   <string name="detail_license_empty">Licenza descoñecida</string> | ||||
|  | @ -170,8 +197,11 @@ | |||
|   <string name="location_permission_title">Pedindo Permiso de Localización</string> | ||||
|   <string name="ok">Aceptar</string> | ||||
|   <string name="warning">Aviso</string> | ||||
|   <string name="duplicate_file_name">Atopouse un nome de ficheiro duplicado</string> | ||||
|   <string name="upload">Enviar</string> | ||||
|   <string name="yes">Si</string> | ||||
|   <string name="no">Non</string> | ||||
|   <string name="media_detail_caption">Lenda</string> | ||||
|   <string name="media_detail_title">Título</string> | ||||
|   <string name="media_detail_description">Descrición</string> | ||||
|   <string name="media_detail_discussion">Conversa</string> | ||||
|  | @ -219,6 +249,8 @@ | |||
|   <string name="upload_problem_different_geolocation">Esta imaxe foi realizada nunha localización diferente.</string> | ||||
|   <string name="upload_problem_fbmd">Por favor sube so fotografías feitas por ti mesmo. Non subas imaxes ou fotografías que atopes nas contas de Facebook de outros.</string> | ||||
|   <string name="upload_problem_do_you_continue">Aínda quere subir esta imaxe?</string> | ||||
|   <string name="upload_connection_error_alert_title">Erro de conexión</string> | ||||
|   <string name="upload_connection_error_alert_detail">O proceso de envío require acceso activo a Internet. Comprobe a súa conexión de rede.</string> | ||||
|   <string name="internet_downloaded">Por favor suba so fotografías feitas por vostede mesmo. Non suba imaxes ou fotografías que descargara da Internet.</string> | ||||
|   <string name="use_external_storage">Gardar fotos tiradas na aplicación</string> | ||||
|   <string name="use_external_storage_summary">Gardar ao almacenamento interno as fotografías tiradas na aplicación</string> | ||||
|  | @ -232,8 +264,8 @@ | |||
|   <string name="nominated_see_more">Ver páxina web para máis detalles</string> | ||||
|   <string name="skip_login">Omitir</string> | ||||
|   <string name="navigation_item_login">Acceder ao sistema</string> | ||||
|   <string name="skip_login_title" fuzzy="true">Realmente quere saltar o inicio de sesión?</string> | ||||
|   <string name="skip_login_message" fuzzy="true">Terá que iniciar sesión para subir imaxes no futuro.</string> | ||||
|   <string name="skip_login_title">Confirma quequere saltar o inicio de sesión?</string> | ||||
|   <string name="skip_login_message">Terá que iniciar sesión para enviar imaxes no futuro.</string> | ||||
|   <string name="login_alert_message">Por favor, inicie a sesión para usar esta funcionalidade</string> | ||||
|   <string name="copy_wikicode">Copiar o texto wiki ó portapapeis</string> | ||||
|   <string name="wikicode_copied">Texto wiki copiado ó portapapeis</string> | ||||
|  | @ -245,6 +277,7 @@ | |||
|   <string name="nearby_commons">COMMONS</string> | ||||
|   <string name="about_rate_us">Avalíenos</string> | ||||
|   <string name="about_faq">FAQ</string> | ||||
|   <string name="user_guide">Guía de uso</string> | ||||
|   <string name="welcome_skip_button">Saltar titorial</string> | ||||
|   <string name="no_internet">Internet non dispoñible</string> | ||||
|   <string name="error_notifications">Erro ó recuperar as notificacións</string> | ||||
|  | @ -257,6 +290,9 @@ | |||
|   <string name="about_translate_cancel">Cancelar</string> | ||||
|   <string name="retry">Reintentar</string> | ||||
|   <string name="showcase_view_whole_nearby_activity">Hai sitios preto de vostede que precisan fotos para ilustrar os seus artigos de Wikipedia</string> | ||||
|   <string name="showcase_view_needs_photo">Este lugar precisa dunha foto.</string> | ||||
|   <string name="showcase_view_has_photo">Este lugar xa ten unha foto.</string> | ||||
|   <string name="showcase_view_no_longer_exists">Este lugar xa non existe.</string> | ||||
|   <string name="no_images_found">Non se atopou ningunha imaxeǃ</string> | ||||
|   <string name="error_loading_images">Houbo un erro ó subir as imaxes.</string> | ||||
|   <string name="image_uploaded_by">Subida porː %1$s</string> | ||||
|  | @ -271,8 +307,10 @@ | |||
|   <string name="error_loading_categories">Houbo un erro ó cargar categorías.</string> | ||||
|   <string name="search_tab_title_media">Multimedia</string> | ||||
|   <string name="search_tab_title_categories">Categorías</string> | ||||
|   <string name="search_tab_title_depictions">Elementos</string> | ||||
|   <string name="explore_tab_title_featured">Destacadas</string> | ||||
|   <string name="explore_tab_title_mobile">Cargada vía móbil</string> | ||||
|   <string name="explore_tab_title_map">Mapa</string> | ||||
|   <string name="successful_wikidata_edit">A imaxe engadiuse a %1$s en Wikidata!</string> | ||||
|   <string name="wikidata_edit_failure">Fallou a actualización da entidade do Wikidata correspondente!</string> | ||||
|   <string name="menu_set_wallpaper">Poñer como imaxe de fondo</string> | ||||
|  | @ -290,21 +328,27 @@ | |||
|   <string name="construction_event_answer">Fotografiás que amosen tecnoloxía ou cultura son moi benvidas en Commons</string> | ||||
|   <string name="congratulatory_message_quiz">Acadou un %1$s de respostas correctas. Parabéns!</string> | ||||
|   <string name="warning_for_no_answer">Escolla unha das dúas opcións para contestar a pregunta</string> | ||||
|   <string name="user_not_logged_in" fuzzy="true">A sesión caducou, por favor inicia unha nova sesión.</string> | ||||
|   <string name="user_not_logged_in">O inicio de sesión caducou. Inicie sesión de novo.</string> | ||||
|   <string name="quiz_result_share_message">Comparta o seu cuestionario cos seus amigos!</string> | ||||
|   <string name="continue_message">Continuar</string> | ||||
|   <string name="correct">Resposta correcta</string> | ||||
|   <string name="wrong">Resposta incorrecta</string> | ||||
|   <string name="quiz_screenshot_question">Pódese subir esta captura de pantalla?</string> | ||||
|   <string name="share_app_title">Compartir a aplicación</string> | ||||
|   <string name="error_fetching_nearby_places" fuzzy="true">Erro ó procurar os lugares próximos.</string> | ||||
|   <string name="rotate">Xirar</string> | ||||
|   <string name="error_fetching_nearby_places">Non foi posíbel cargar lugares próximos</string> | ||||
|   <string name="no_pictures_in_this_area">Non hai imaxes nesta zona</string> | ||||
|   <string name="no_nearby_places_around">Non hai lugares próximos</string> | ||||
|   <string name="error_fetching_nearby_monuments">Produciuse un erro ao buscar monumentos próximos.</string> | ||||
|   <string name="no_recent_searches">Non hai procuras recentes</string> | ||||
|   <string name="delete_recent_searches_dialog">Está seguro de querer borrar o seu historial de procuras?</string> | ||||
|   <string name="cancel_upload_dialog">Confirma que quere cancelar este envío?</string> | ||||
|   <string name="delete_search_dialog">Queres borrar esta procura?</string> | ||||
|   <string name="search_history_deleted">Eliminouse o historial de procuras</string> | ||||
|   <string name="nominate_delete">Nomear para borrado</string> | ||||
|   <string name="delete">Borrar</string> | ||||
|   <string name="Achievements">Logros</string> | ||||
|   <string name="Profile">Perfil</string> | ||||
|   <string name="statistics">Estatísticas</string> | ||||
|   <string name="statistics_thanks">Agradecementos recibidos</string> | ||||
|   <string name="statistics_featured">Imaxes destacadas</string> | ||||
|  | @ -355,18 +399,22 @@ | |||
|   <string name="no_uploads">Dámoslle a benvida ó Commonsǃ\n\nCargue o seu primeiro ficheiro premendo no botón Engadir.</string> | ||||
|   <string name="no_categories_selected">Non hai categorías seleccionadas</string> | ||||
|   <string name="no_categories_selected_warning_desc">As imaxes sen categorías só son utilizables en contadas ocasións. Está seguro de que quere continuar sen seleccionar categorías?</string> | ||||
|   <string name="upload_flow_all_images_in_set" fuzzy="true">(Para tódalas imaxes no conxunto)</string> | ||||
|   <string name="back_button_warning">Cancelar envío</string> | ||||
|   <string name="back_button_continue">Continuar co envío</string> | ||||
|   <string name="upload_flow_all_images_in_set">(Para tódalas imaxes do conxunto)</string> | ||||
|   <string name="search_this_area">Procurar nesta área</string> | ||||
|   <string name="nearby_card_permission_title">Solicitude de permisos</string> | ||||
|   <string name="nearby_card_permission_explanation">Desexa que usemos a súa localizacións actual para amosarlle o lugar máis preto que precisa imaxes?</string> | ||||
|   <string name="unable_to_display_nearest_place">Imposible amosar o sitio máis achegado que precisa fotos sen ter permisos de localización</string> | ||||
|   <string name="never_ask_again">Non volver a preguntar isto nunca</string> | ||||
|   <string name="display_location_permission_title" fuzzy="true">Amosar permiso de localización</string> | ||||
|   <string name="display_location_permission_title">Solicitar permiso de localización</string> | ||||
|   <string name="display_location_permission_explanation">Pedir permisos de localización cando sexa necesario para a funcionalidade de notificación de proximidade.</string> | ||||
|   <string name="achievements_fetch_failed" fuzzy="true">Algo foi mal, non puidemos obter as túas achegas</string> | ||||
|   <string name="ends_on">Finaliza o:</string> | ||||
|   <string name="display_campaigns">Amosar campañas</string> | ||||
|   <string name="display_campaigns_explanation">Ver as campañas en curso</string> | ||||
|   <string name="option_allow">Permitir</string> | ||||
|   <string name="option_dismiss">Descartar</string> | ||||
|   <string name="nearby_campaign_dismiss_message">Xa non verá as campañas. Porén, pode volver habilitar esta notificación na configuración.</string> | ||||
|   <string name="this_function_needs_network_connection" fuzzy="true">Esta función require conexión de rede, verifique a súa configuración de conexión.</string> | ||||
|   <string name="error_processing_image">Houbo un erro ó procesar a imaxe. Por favor, ténteo de novoǃ</string> | ||||
|  | @ -383,8 +431,8 @@ | |||
|   <string name="send_thank_success_title">Enviando agradecementos: Éxito</string> | ||||
|   <string name="send_thank_success_message">Enviado correctamente o agradecemento a %1$s</string> | ||||
|   <string name="send_thank_toast">Enviando agradecementos por %1$s</string> | ||||
|   <string name="review_thanks_yes_button_text" fuzzy="true">Si, por que non</string> | ||||
|   <string name="review_thanks_no_button_text" fuzzy="true">Seguinte imaxe</string> | ||||
|   <string name="review_thanks_yes_button_text">Imaxe seguinte</string> | ||||
|   <string name="review_thanks_no_button_text">Si, por que non</string> | ||||
|   <string name="no_image">Ningunha imaxe usada</string> | ||||
|   <string name="no_image_reverted">Ningunha imaxe revertida</string> | ||||
|   <string name="no_image_uploaded">Ningunha imaxe subida</string> | ||||
|  | @ -396,6 +444,8 @@ | |||
|   <string name="error_occurred_in_picking_images">Houbo un erro ó escoller as imaxes</string> | ||||
|   <string name="please_wait">Por favor, agarde…</string> | ||||
|   <string name="skip_image">Saltar esta imaxe</string> | ||||
|   <string name="manage_exif_tags">Xestionar etiquetas EXIF</string> | ||||
|   <string name="manage_exif_tags_summary">Seleccione que etiquetas EXIF manter nos envíos</string> | ||||
|   <string name="exif_tag_name_author">Autor</string> | ||||
|   <string name="exif_tag_name_copyright">Dereitos de autoría</string> | ||||
|   <string name="exif_tag_name_location">Localización</string> | ||||
|  | @ -407,16 +457,43 @@ | |||
|   <string name="image_info">Información da imaxe</string> | ||||
|   <string name="no_categories_found">Non se atoparon categorías</string> | ||||
|   <string name="upload_cancelled">Cancelouse a carga</string> | ||||
|   <string name="default_description_language">Lingua de descrición predeterminada</string> | ||||
|   <string name="delete_helper_show_deletion_title">Nomeando para borrado</string> | ||||
|   <string name="delete_helper_show_deletion_title_success">Todo correcto</string> | ||||
|   <string name="delete_helper_show_deletion_title_failed">Fallou</string> | ||||
|   <string name="delete_helper_ask_spam_selfie" fuzzy="true">Un autorretrato</string> | ||||
|   <string name="delete_helper_show_deletion_message_else">Non foi posíbel solicitar a eliminación.</string> | ||||
|   <string name="delete_helper_ask_spam_selfie">Un autorretrato que non se emprega en ningún artigo</string> | ||||
|   <string name="delete_helper_ask_spam_blurry" fuzzy="true">Borrosa</string> | ||||
|   <string name="delete_helper_ask_spam_nonsense" fuzzy="true">Sen sentido</string> | ||||
|   <string name="delete_helper_ask_reason_copyright_press_photo">Foto de prensa</string> | ||||
|   <string name="delete_helper_ask_reason_copyright_internet_photo">Foto aleatoria de internet</string> | ||||
|   <string name="delete_helper_ask_reason_copyright_logo">Logo</string> | ||||
|   <string name="delete_helper_ask_alert_set_positive_button_reason">Porque é</string> | ||||
|   <string name="category_edit_helper_show_edit_title_success">Todo correcto</string> | ||||
|   <plurals name="category_edit_helper_show_edit_message_if"> | ||||
|     <item quantity="one">Engádese a categoría %1$s .</item> | ||||
|     <item quantity="other">Engádense as categorías %1$s .</item> | ||||
|   </plurals> | ||||
|   <string name="category_edit_helper_edit_message_else">Non foi posíbel engadir categorías.</string> | ||||
|   <string name="category_edit_button_text">Actualizar categorías</string> | ||||
|   <string name="depictions_edit_helper_make_edit_toast">A tentar actualizar representacións.</string> | ||||
|   <string name="depictions_edit_helper_show_edit_title">Editar representacións</string> | ||||
|   <plurals name="depictions_edit_helper_show_edit_message_if"> | ||||
|     <item quantity="one">Engádese a representación %1$s .</item> | ||||
|     <item quantity="other">Engádense as representacións %1$s .</item> | ||||
|   </plurals> | ||||
|   <string name="depictions_edit_helper_edit_message_else">Non foi posíbel engadir representacións.</string> | ||||
|   <string name="coordinates_edit_helper_make_edit_toast">A tentar actualizar coordenadas.</string> | ||||
|   <string name="coordinates_edit_helper_show_edit_title">Actualización de coordenadas</string> | ||||
|   <string name="description_edit_helper_show_edit_title">Actualización da descrición</string> | ||||
|   <string name="caption_edit_helper_show_edit_title">Actualización da lenda</string> | ||||
|   <string name="coordinates_edit_helper_show_edit_title_success">Todo correcto</string> | ||||
|   <string name="coordinates_edit_helper_show_edit_message">Engádense as coordenadas %1$s .</string> | ||||
|   <string name="description_edit_helper_show_edit_message">Engádense as descricións.</string> | ||||
|   <string name="caption_edit_helper_show_edit_message">Engádese a lenda.</string> | ||||
|   <string name="coordinates_edit_helper_edit_message_else">Non foi posíbel engadir as coordenadas.</string> | ||||
|   <string name="description_edit_helper_edit_message_else">Non foi posíbel engadir descricións.</string> | ||||
|   <string name="caption_edit_helper_edit_message_else">Non foi posíbel engadir lenda.</string> | ||||
|   <string name="share_image_via">Compartir imaxe vía</string> | ||||
|   <string name="account_created">Conta creada!</string> | ||||
|   <string name="place_state_exists">Existe</string> | ||||
|  | @ -429,4 +506,28 @@ | |||
|   <string name="setting_wallpaper_dialog_title">Definir como fondo de pantalla</string> | ||||
|   <string name="theme_dark_name">Escuro</string> | ||||
|   <string name="theme_light_name">Claro</string> | ||||
|   <string name="todo_improve">Melloras suxeridas:</string> | ||||
|   <string name="missing_category">- Engadir categorías a esta imaxe para mellorar a usabilidade.</string> | ||||
|   <string name="missing_article">- Engade esta imaxe ao artigo asociado da Wikipedia que non ten imaxes.</string> | ||||
|   <string name="add_picture_to_wikipedia_article_title">Engadir imaxe á Wikipedia</string> | ||||
|   <string name="confirm">Confirmar</string> | ||||
|   <string name="instructions_title">Instrucións</string> | ||||
|   <string name="wikipedia_instructions_step_7">7. Publicar o artigo</string> | ||||
|   <string name="pause">pausar</string> | ||||
|   <string name="resume">continuar</string> | ||||
|   <string name="paused">En pausa</string> | ||||
|   <string name="more">Máis</string> | ||||
|   <string name="bookmarks">Marcadores</string> | ||||
|   <string name="achievements_tab_title">Logros</string> | ||||
|   <string name="leaderboard_tab_title">Tǃboa de maior actividade</string> | ||||
|   <string name="rank_prefix">Clasificaciónː</string> | ||||
|   <string name="count_prefix">Número:</string> | ||||
|   <string name="leaderboard_column_rank">Clasificación</string> | ||||
|   <string name="menu_set_avatar">Establecer como avatar</string> | ||||
|   <string name="leaderboard_yearly">Anualmente</string> | ||||
|   <string name="leaderboard_weekly">Semanalmente</string> | ||||
|   <string name="leaderboard_all_time">Todo o tempo</string> | ||||
|   <string name="leaderboard_upload">Enviar</string> | ||||
|   <string name="leaderboard_my_rank_button_text">A miña clasificación</string> | ||||
|   <string name="limited_connection_enabled">Activouse o modo de conexión limitadoǃ</string> | ||||
| </resources> | ||||
|  |  | |||
|  | @ -4,6 +4,7 @@ | |||
| * Abijeet Patro | ||||
| * Anamdas | ||||
| * Anandra | ||||
| * AnupamM | ||||
| * Bhatakati aatma | ||||
| * Gopalindians | ||||
| * Nilesh shukla | ||||
|  | @ -349,4 +350,9 @@ | |||
|   <string name="leaderboard_column_count">गणना</string> | ||||
|   <string name="custom_selector_dismiss_limit_warning_button_text">रद्द करें</string> | ||||
|   <string name="talk">वार्ता</string> | ||||
|   <string name="are_you_sure_that_you_want_cancel_all_the_uploads">क्या आप वाकई सभी अपलोड रद्द करना चाहते हैं?</string> | ||||
|   <string name="cancelling_all_the_uploads">सभी अपलोड रद्द किये जा रहे हैं...</string> | ||||
|   <string name="uploads">अपलोड</string> | ||||
|   <string name="pending">लंबित</string> | ||||
|   <string name="failed">विफल हुआ</string> | ||||
| </resources> | ||||
|  |  | |||
|  | @ -5,6 +5,7 @@ | |||
| * Arifin.wijaya | ||||
| * DARMAS BUDI SANTOSO | ||||
| * Daud I.F. Argana | ||||
| * Fafau06 | ||||
| * Farras | ||||
| * Gombang | ||||
| * Hidayatsrf | ||||
|  | @ -240,6 +241,7 @@ | |||
|   <string name="navigation_item_about">Perihal</string> | ||||
|   <string name="navigation_item_settings">Pengaturan</string> | ||||
|   <string name="navigation_item_feedback">Umpan balik</string> | ||||
|   <string name="navigation_item_feedback_github">Ulasan melalui GitHub</string> | ||||
|   <string name="navigation_item_logout">Keluar</string> | ||||
|   <string name="navigation_item_info">Tutorial</string> | ||||
|   <string name="navigation_item_notification">Pemberitahuan</string> | ||||
|  | @ -349,6 +351,7 @@ | |||
|   <string name="share_app_title">Bagikan Aplikasi</string> | ||||
|   <string name="rotate">Putar</string> | ||||
|   <string name="error_fetching_nearby_places" fuzzy="true">Galat saat mengambil tempat terdekat.</string> | ||||
|   <string name="no_pictures_in_this_area">Tidak ada gambar di area ini</string> | ||||
|   <string name="no_nearby_places_around">Tidak ditemukan tempat yang dekat</string> | ||||
|   <string name="error_fetching_nearby_monuments">Galat saat mengambil monumen terdekat.</string> | ||||
|   <string name="no_recent_searches">Tidak ada pencarian terbaru</string> | ||||
|  | @ -729,7 +732,19 @@ | |||
|   <string name="learn_how_to_write_a_useful_description">Pelajari cara menulis deskripsi yang berguna</string> | ||||
|   <string name="learn_how_to_write_a_useful_caption">Pelajari cara menulis takarir yang berguna</string> | ||||
|   <string name="see_your_achievements">Lihat pencapaian Anda</string> | ||||
|   <string name="edit_image">Edit Gambar</string> | ||||
|   <string name="edit_location">Edit Lokasi</string> | ||||
|   <string name="location_updated">Lokasi diperbarui!</string> | ||||
|   <string name="remove_location">Hapus Lokasi</string> | ||||
|   <string name="remove_location_warning_title">Hapus Peringatan Lokasi</string> | ||||
|   <string name="remove_location_warning_desc">Lokasi membuat gambar lebih berguna dan mudah ditemukan. Apakah Anda benar-benar ingin menghapus lokasi dari gambar ini?</string> | ||||
|   <string name="location_removed">Lokasi dihapus!</string> | ||||
|   <plurals name="custom_picker_images_selected_title_appendix"> | ||||
|     <item quantity="other">%d gambar dipilih</item> | ||||
|   </plurals> | ||||
|   <string name="talk">Bicara</string> | ||||
|   <string name="cancelling_all_the_uploads">Membatalkan semua unggahan...</string> | ||||
|   <string name="uploads">Unggah</string> | ||||
|   <string name="pending">Menunggu</string> | ||||
|   <string name="failed">Gagal</string> | ||||
| </resources> | ||||
|  |  | |||
|  | @ -8,6 +8,7 @@ | |||
| * Black Sky83 | ||||
| * Champ0999 | ||||
| * Davio | ||||
| * Dream Indigo | ||||
| * Gianfranco | ||||
| * Lorelai87 | ||||
| * Lorem Ipsum | ||||
|  | @ -127,7 +128,7 @@ | |||
|   <string name="share_caption_hint">Didascalia</string> | ||||
|   <string name="login_failed_network">Impossibile accedere: errore di rete</string> | ||||
|   <string name="login_failed_throttled">Troppi tentativi falliti. Riprova tra alcuni minuti.</string> | ||||
|   <string name="login_failed_blocked">Spiacente, questo utente è stato bloccato su Commons</string> | ||||
|   <string name="login_failed_blocked">Spiacente, quest\'utente è stato/a bloccato/a su Commons</string> | ||||
|   <string name="login_failed_2fa_needed">Devi fornire il tuo codice di autenticazione a due fattori.</string> | ||||
|   <string name="login_failed_generic">Accesso non riuscito</string> | ||||
|   <string name="share_upload_button">Carica</string> | ||||
|  | @ -737,9 +738,9 @@ | |||
|   <string name="menu_view_set_white_background">Imposta lo sfondo bianco</string> | ||||
|   <string name="menu_view_set_black_background">Imposta lo sfondo nero</string> | ||||
|   <string name="report_violation">Segnala violazione</string> | ||||
|   <string name="report_user">Segnala questo utente</string> | ||||
|   <string name="report_user">Segnala quest\'utente</string> | ||||
|   <string name="report_content">Segnala questo contenuto</string> | ||||
|   <string name="request_user_block">Richiedi di bloccare questo utente</string> | ||||
|   <string name="request_user_block">Richiedi di bloccare quest\'utente</string> | ||||
|   <string name="welcome_to_full_screen_mode_text">Benvenuto nella modalità di selezione a schermo intero</string> | ||||
|   <string name="full_screen_mode_zoom_info">Usa due dita per ingrandire e rimpicciolire.</string> | ||||
|   <string name="full_screen_mode_features_info">Scorri velocemente e a lungo per eseguire queste azioni: \n- Sinistra/destra: vai al precedente/successivo \n- Su: seleziona\n- Giù: contrassegna come da non caricare.</string> | ||||
|  |  | |||
|  | @ -815,5 +815,10 @@ | |||
|   <string name="is_at_a_different_place_please_specify_the_correct_place_below_if_possible_tell_us_the_correct_latitude_longitude">\"%1$s\" נמצא במקום אחר. נא לציין את המקום הנכון למטה, ואם אפשר, לכתוב את קו הרוחב ואת קו האורך הנכונים.</string> | ||||
|   <string name="other_problem_or_information_please_explain_below">בעיה אחרת או מידע אחר (נא להסביר הלאה).</string> | ||||
|   <string name="feedback_destination_note">המשוב שלך מתפרסם בדף הוויקי הבא: <a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Mobile app/Feedback</a></string> | ||||
|   <string name="are_you_sure_that_you_want_cancel_all_the_uploads">האם ברצונך באמת לבטל את כל ההעלאות?</string> | ||||
|   <string name="cancelling_all_the_uploads">ביטול כל ההעלאות...</string> | ||||
|   <string name="uploads">העלאות</string> | ||||
|   <string name="pending">ממתינות</string> | ||||
|   <string name="failed">נכשלו</string> | ||||
|   <string name="could_not_load_place_data">לא היה אפשר לטעון את נתוני המקום</string> | ||||
| </resources> | ||||
|  |  | |||
|  | @ -779,5 +779,10 @@ | |||
|   <string name="is_at_a_different_place_please_specify_the_correct_place_below_if_possible_tell_us_the_correct_latitude_longitude">„%1$s“ се наоѓа на друго место. Подолу укажете го исправното место и, ако е можно, ставете исправна географска ширина и должина.</string> | ||||
|   <string name="other_problem_or_information_please_explain_below">Друг проблем или информација (објаснете подолу).</string> | ||||
|   <string name="feedback_destination_note">Вашите мислења се објавуваат на следнава викистраница:  <a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Mobile app/Feedback</a></string> | ||||
|   <string name="are_you_sure_that_you_want_cancel_all_the_uploads">Дали сигурно сакате да ги откажете сите подигања?</string> | ||||
|   <string name="cancelling_all_the_uploads">Ги откажувам сите подигања...</string> | ||||
|   <string name="uploads">Подигања</string> | ||||
|   <string name="pending">Во исчекување</string> | ||||
|   <string name="failed">Неуспешно</string> | ||||
|   <string name="could_not_load_place_data">Не можев да ги вчитам податоците за место</string> | ||||
| </resources> | ||||
|  |  | |||
|  | @ -50,7 +50,7 @@ | |||
|   <string name="login_success" fuzzy="true">လုက်အေန် အာစိုပ်ဒတုဲ!</string> | ||||
|   <string name="login_failed" fuzzy="true">လံက်အေန် လီုလာ်!</string> | ||||
|   <string name="upload_failed">ဝှာင် ဟွံဂွံဆဵု၊ ပဂုန်တုဲ ဂၠာဲ ဝှာင်တၞဟ်။</string> | ||||
|   <string name="authentication_failed" fuzzy="true">ပွမစၟဳစၟတ်ဂှ် ဟွံအံင်ဇၞး။ ပဂုန်တုဲ လံက်အေန် မွဲဝါပၠန်</string> | ||||
|   <string name="authentication_failed">ပွမစၟဳစၟတ်ဂှ် ဟွံအံင်ဇၞး။ ပဂုန်တုဲ လံက်အေန် မွဲဝါပၠန်</string> | ||||
|   <string name="uploading_started">ပတိုန်ဝှာင် စဒၟံင်ရ!</string> | ||||
|   <string name="upload_completed_notification_title">%1$s ပတိုန်ပၠုပ်တုဲ!</string> | ||||
|   <string name="upload_completed_notification_text">ဒၞာဲမကလေင်ရံင် ဝှာင်ပတိုန်ပၠုပ် မၞး</string> | ||||
|  | @ -69,11 +69,12 @@ | |||
|   <string name="menu_nearby">ဗဒဲါဒၞာဲဏအ်</string> | ||||
|   <string name="provider_contributions">ပတိုန်ပၠုပ် ဇကုဂမၠိုင်</string> | ||||
|   <string name="menu_share">ပါ်ပရအ်</string> | ||||
|   <string name="menu_view_file_page">ဗဵု မုက်လိက် ဝှာင်</string> | ||||
|   <string name="share_title_hint">က္ဍိုပ်လိက် (အာတ်မိက်ဒၟံင်)</string> | ||||
|   <string name="add_caption_toast">ပဂုန်တုဲ ကဵု က္ဍိုပ်လိက် ဝှာင်ဏအ်ညိ</string> | ||||
|   <string name="share_description_hint">မဗမံက်ထ္ၜး</string> | ||||
|   <string name="share_caption_hint" fuzzy="true">က္ဍိုပ်လိက် (ပိုင်ခြာလဝ် လၟိဟ်မလိက် ၂၅၅)</string> | ||||
|   <string name="login_failed_network" fuzzy="true">လုပ်လံက်အေန် ဟွံဂွံ - ဇာဇၞိက် ဗၠေတ်</string> | ||||
|   <string name="share_caption_hint">က္ဍိုပ်လိက်</string> | ||||
|   <string name="login_failed_network">လုပ်လံက်အေန် ဟွံဂွံ - ဇာဇၞိက် ဗၠေတ်</string> | ||||
|   <string name="login_failed_throttled">ပရေင်ဂိုတ်ဂစာန် ဟွံအံင်ဇၞး ဂၠိုင်လောန်အာရ။ ပဂုန်တုဲ မိနေတ်ညိညပၠန် ကလေင်စမ်ပၠန်။</string> | ||||
|   <string name="login_failed_blocked">သၠးအခေါင်၊ ညးလွပ်ဏအ် ဒးဒုင်ကၟာတ်လဒဵုလဝ် ပ္ဍဲ ကောမ်မောန်</string> | ||||
|   <string name="login_failed_2fa_needed">ကုစၟဳစၟတ်မၞးၜါဂှ် သ္ဒးပါ်လဝ် ဗွဲတၞဟ်ခြာရောင်။</string> | ||||
|  | @ -85,7 +86,7 @@ | |||
|   <string name="categories_search_text_hint">ဂၠာဲ ကဏ္ဍဂမၠိုၚ်</string> | ||||
|   <string name="depicts_search_text_hint">ဂၠာဲ တင်ဂၞင် မၞိဟ်မဗၟံက်ထ္ၜး (မပတံ ဒဵု၊ ဍုင်လ္ဂုင်)</string> | ||||
|   <string name="menu_save_categories">ဂိုင်သိပ်</string> | ||||
|   <string name="refresh_button">ကလေင်မၚုဟ်</string> | ||||
|   <string name="refresh_button">ကလေင်မင္ၚုဟ်</string> | ||||
|   <string name="display_list_button">စရၚ်</string> | ||||
|   <string name="contributions_subtitle_zero" fuzzy="true">ဟွံဂွံ ပတိုန်ပၠုပ်ဏီ</string> | ||||
|   <string name="categories_not_found">ကဏ္ဍ မကိတ်ညဳ ကု %1$s ဟွံဆဵု</string> | ||||
|  |  | |||
|  | @ -800,5 +800,10 @@ | |||
|   <string name="is_at_a_different_place_please_specify_the_correct_place_below_if_possible_tell_us_the_correct_latitude_longitude">‘%1$s’ bevindt zich ergens anders. Geef hieronder de juiste plaats aan en noteer, indien mogelijk, de juiste breedte- en lengtegraad.</string> | ||||
|   <string name="other_problem_or_information_please_explain_below">Ander probleem of andere informatie (verklaar hieronder).</string> | ||||
|   <string name="feedback_destination_note">Uw feedback wordt op de volgende wikipagina geplaatst: <a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Mobile app/Feedback</a></string> | ||||
|   <string name="are_you_sure_that_you_want_cancel_all_the_uploads">Weet u zeker dat u alle uploads wilt annuleren?</string> | ||||
|   <string name="cancelling_all_the_uploads">Alle uploads worden geannuleerd…</string> | ||||
|   <string name="uploads">Uploads</string> | ||||
|   <string name="pending">In behandeling</string> | ||||
|   <string name="failed">Mislukt</string> | ||||
|   <string name="could_not_load_place_data">Plaatsgegevens konden niet geladen worden</string> | ||||
| </resources> | ||||
|  |  | |||
|  | @ -775,5 +775,10 @@ | |||
|   <string name="is_at_a_different_place_please_specify_the_correct_place_below_if_possible_tell_us_the_correct_latitude_longitude">\'%1$s\' a l\'é ant un pòst diferent. Për piasì, ch\'a spessìfica ël pòst giust sì-sota e, si possìbil, ch\'a scriva latitùdin e longitùdin giuste.</string> | ||||
|   <string name="other_problem_or_information_please_explain_below">Àutr problema o anformassion (për piasì, ch\'a spiega sì-sota).</string> | ||||
|   <string name="feedback_destination_note">Ij sò sugeriment a saran giontà a coste pàgine wiki:  <a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Mobile app/Feedback</a></string> | ||||
|   <string name="are_you_sure_that_you_want_cancel_all_the_uploads">É-lo sigur ëd vorèj anulé tuti ij cariament?</string> | ||||
|   <string name="cancelling_all_the_uploads">Anulament ëd tuti ij cariament...</string> | ||||
|   <string name="uploads">Cariament</string> | ||||
|   <string name="pending">An atèisa</string> | ||||
|   <string name="failed">Falì</string> | ||||
|   <string name="could_not_load_place_data">Impossìbil carié ij dàit dël pòst</string> | ||||
| </resources> | ||||
|  |  | |||
|  | @ -18,6 +18,7 @@ | |||
| * Jesusmc | ||||
| * Kaganer | ||||
| * Kareyac | ||||
| * Lutece398 | ||||
| * MaxBioHazard | ||||
| * McDutchie | ||||
| * Megakott | ||||
|  | @ -839,5 +840,8 @@ | |||
|   <string name="is_at_a_different_place_please_specify_the_correct_place_below_if_possible_tell_us_the_correct_latitude_longitude">\'%1$s\' находится в другом месте. Пожалуйста, укажите правильное место ниже и, если возможно, напишите правильную широту и долготу.</string> | ||||
|   <string name="other_problem_or_information_please_explain_below">Другая проблема или информация (пожалуйста, объясните ниже).</string> | ||||
|   <string name="feedback_destination_note">Ваш отзыв будет опубликован на следующей вики-странице: <a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Mobile app/Feedback</a></string> | ||||
|   <string name="are_you_sure_that_you_want_cancel_all_the_uploads">Вы уверены, что хотите отменить все загрузки?</string> | ||||
|   <string name="cancelling_all_the_uploads">Отмена всех загрузок...</string> | ||||
|   <string name="uploads">Загрузки</string> | ||||
|   <string name="could_not_load_place_data">Не удалось загрузить данные о месте</string> | ||||
| </resources> | ||||
|  |  | |||
|  | @ -270,4 +270,7 @@ | |||
|   <string name="menu_view_report">رپورٹ</string> | ||||
|   <string name="error_sending_thanks">مصنف کوں شکریہ بھیڄݨ وچ خرابی۔</string> | ||||
|   <string name="talk">ڳالھ مہاڑ</string> | ||||
|   <string name="uploads">اپلوڈاں</string> | ||||
|   <string name="pending">وچار ہیٹھ</string> | ||||
|   <string name="failed">ناکام تھیا</string> | ||||
| </resources> | ||||
|  |  | |||
|  | @ -735,4 +735,7 @@ | |||
|   <string name="please_enter_some_comments">Унесите коментар</string> | ||||
|   <string name="talk">Разговор</string> | ||||
|   <string name="does_not_exist_anymore_no_picture_can_ever_be_taken_of_it">„%1$s” не постоји више, и није га могуће више сликати.</string> | ||||
|   <string name="uploads">Отпремања</string> | ||||
|   <string name="pending">На чекању</string> | ||||
|   <string name="failed">Није успело</string> | ||||
| </resources> | ||||
|  |  | |||
|  | @ -783,5 +783,10 @@ | |||
|   <string name="is_at_a_different_place_please_specify_the_correct_place_below_if_possible_tell_us_the_correct_latitude_longitude">\"%1$s\" är på en annan plats. Ange den korrekta platsen nedan samt ange latitud och longitud om det är möjligt.</string> | ||||
|   <string name="other_problem_or_information_please_explain_below">Andra problem eller information (ange nedan).</string> | ||||
|   <string name="feedback_destination_note">Din återkoppling kommer att skickas till följande wikisida:  <a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Mobilapp/Återkoppling</a></string> | ||||
|   <string name="are_you_sure_that_you_want_cancel_all_the_uploads">Är du säker på att du vill avbryta alla uppladdningar?</string> | ||||
|   <string name="cancelling_all_the_uploads">Avbryter alla uppladdningar...</string> | ||||
|   <string name="uploads">Uppladdningar</string> | ||||
|   <string name="pending">Pågår</string> | ||||
|   <string name="failed">Misslyckades</string> | ||||
|   <string name="could_not_load_place_data">Kunde inte läsa in platsdata</string> | ||||
| </resources> | ||||
|  |  | |||
|  | @ -800,5 +800,10 @@ | |||
|   <string name="is_at_a_different_place_please_specify_the_correct_place_below_if_possible_tell_us_the_correct_latitude_longitude">「%1$s」位於不同的位置。請在下面指定正確的位置,可以的話請填寫正確的經緯度。</string> | ||||
|   <string name="other_problem_or_information_please_explain_below">其他問題或資訊(請在下方解釋)。</string> | ||||
|   <string name="feedback_destination_note">您的回饋已發布到以下 wiki 頁面:<a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Mobile app/Feedback</a></string> | ||||
|   <string name="are_you_sure_that_you_want_cancel_all_the_uploads">您確定要取消所有上傳嗎?</string> | ||||
|   <string name="cancelling_all_the_uploads">正在取消所有上傳…</string> | ||||
|   <string name="uploads">上傳</string> | ||||
|   <string name="pending">待處理</string> | ||||
|   <string name="failed">失敗</string> | ||||
|   <string name="could_not_load_place_data">無法載入地點資料</string> | ||||
| </resources> | ||||
|  |  | |||
|  | @ -42,6 +42,7 @@ | |||
|     <attr name="more_bottom_sheet_drawable_color" format="reference"/> | ||||
|     <attr name="card_item_color" format="reference"/> | ||||
|     <attr name="notification_icon_drawable" format="reference"/> | ||||
|     <attr name="upload_icon_drawable" format="reference"/> | ||||
|     <attr name="notification_icon_text_color" format="reference"/> | ||||
|     <attr name="toggle_theme" format="reference"/> | ||||
|     <attr name="contributionsListTextSecondary" format="reference"/> | ||||
|  |  | |||
|  | @ -826,5 +826,10 @@ Upload your first media by tapping on the add button.</string> | |||
|   <string name="is_at_a_different_place_please_specify_the_correct_place_below_if_possible_tell_us_the_correct_latitude_longitude">\'%1$s\' is at a different place. Please specify the correct place below, and if possible, write the correct latitude and longitude.</string> | ||||
|   <string name="other_problem_or_information_please_explain_below">Other problem or information (please explain below).</string> | ||||
|   <string name="feedback_destination_note">Your feedback gets posted to the following wiki page: <![CDATA[ <a href="https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback">Commons:Mobile app/Feedback</a> ]]></string> | ||||
|   <string name="are_you_sure_that_you_want_cancel_all_the_uploads">Are you sure that you want cancel all the uploads?</string> | ||||
|   <string name="cancelling_all_the_uploads">Cancelling all the uploads...</string> | ||||
|   <string name="uploads">Uploads</string> | ||||
|   <string name="pending">Pending</string> | ||||
|   <string name="failed">Failed</string> | ||||
|   <string name="could_not_load_place_data">Could not load place data</string> | ||||
| </resources> | ||||
|  |  | |||
|  | @ -48,6 +48,7 @@ | |||
|         <item name="more_bottom_sheet_style">@style/DarkMoreBottomSheetStyle</item> | ||||
|         <item name="more_bottom_sheet_drawable_color">@color/white</item> | ||||
|         <item name="card_item_color">@color/white</item> | ||||
|         <item name="upload_icon_drawable">@drawable/ic_upload_white_24dp</item> | ||||
|         <item name="notification_icon_drawable">@drawable/ic_notifications_white_24dp</item> | ||||
|         <item name="notification_icon_text_color">@color/white</item> | ||||
|         <item name="toggle_theme">@style/SwitchThemeDark</item> | ||||
|  | @ -108,6 +109,7 @@ | |||
|         <item name="more_bottom_sheet_style">@style/LightMoreBottomSheetStyle</item> | ||||
|         <item name="more_bottom_sheet_drawable_color">@color/black</item> | ||||
|         <item name="card_item_color">@color/primaryDarkColor</item> | ||||
|         <item name="upload_icon_drawable">@drawable/ic_upload_blue_24dp</item> | ||||
|         <item name="notification_icon_drawable">@drawable/ic_notifications_blue_24dp</item> | ||||
|         <item name="notification_icon_text_color">@color/primaryDarkColor</item> | ||||
|         <item name="toggle_theme">@style/SwitchThemeLight</item> | ||||
|  |  | |||
|  | @ -92,63 +92,6 @@ class ContributionViewHolderUnitTests { | |||
|         Assert.assertNotNull(contributionViewHolder) | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     @Throws(Exception::class) | ||||
|     fun testSetResume() { | ||||
|         Shadows.shadowOf(Looper.getMainLooper()).idle() | ||||
|         val method: Method = ContributionViewHolder::class.java.getDeclaredMethod( | ||||
|             "setResume" | ||||
|         ) | ||||
|         method.isAccessible = true | ||||
|         method.invoke(contributionViewHolder) | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     @Throws(Exception::class) | ||||
|     fun testSetPaused() { | ||||
|         Shadows.shadowOf(Looper.getMainLooper()).idle() | ||||
|         val method: Method = ContributionViewHolder::class.java.getDeclaredMethod( | ||||
|             "setPaused" | ||||
|         ) | ||||
|         method.isAccessible = true | ||||
|         method.invoke(contributionViewHolder) | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     @Throws(Exception::class) | ||||
|     fun testPause() { | ||||
|         Shadows.shadowOf(Looper.getMainLooper()).idle() | ||||
|         val method: Method = ContributionViewHolder::class.java.getDeclaredMethod( | ||||
|             "pause" | ||||
|         ) | ||||
|         method.isAccessible = true | ||||
|         method.invoke(contributionViewHolder) | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     @Throws(Exception::class) | ||||
|     fun testResume() { | ||||
|         Shadows.shadowOf(Looper.getMainLooper()).idle() | ||||
|         val method: Method = ContributionViewHolder::class.java.getDeclaredMethod( | ||||
|             "resume" | ||||
|         ) | ||||
|         method.isAccessible = true | ||||
|         method.invoke(contributionViewHolder) | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     @Throws(Exception::class) | ||||
|     fun testOnPauseResumeButtonClickedCaseTrue() { | ||||
|         contributionViewHolder.onPauseResumeButtonClicked() | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     @Throws(Exception::class) | ||||
|     fun testOnPauseResumeButtonClickedCaseFalse() { | ||||
|         bindind.pauseResumeButton.tag = "" | ||||
|         contributionViewHolder.onPauseResumeButtonClicked() | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     @Throws(Exception::class) | ||||
|     fun testWikipediaButtonClicked() { | ||||
|  | @ -161,18 +104,6 @@ class ContributionViewHolderUnitTests { | |||
|         contributionViewHolder.imageClicked() | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     @Throws(Exception::class) | ||||
|     fun testDeleteUpload() { | ||||
|         contributionViewHolder.deleteUpload() | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     @Throws(Exception::class) | ||||
|     fun testRetryUpload() { | ||||
|         contributionViewHolder.retryUpload() | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     @Throws(Exception::class) | ||||
|     fun testChooseImageSource() { | ||||
|  | @ -240,17 +171,6 @@ class ContributionViewHolderUnitTests { | |||
|         contributionViewHolder.init(0, contribution) | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     @Throws(Exception::class) | ||||
|     fun testInitCaseNonNull_STATE_QUEUED_LIMITED_CONNECTION_MODE() { | ||||
|         Shadows.shadowOf(Looper.getMainLooper()).idle() | ||||
|         `when`(contribution.state).thenReturn(Contribution.STATE_QUEUED_LIMITED_CONNECTION_MODE) | ||||
|         `when`(contribution.media).thenReturn(media) | ||||
|         `when`(media.mostRelevantCaption).thenReturn("") | ||||
|         `when`(media.author).thenReturn("") | ||||
|         contributionViewHolder.init(0, contribution) | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     @Throws(Exception::class) | ||||
|     fun testInitCaseNonNull_STATE_IN_PROGRESS() { | ||||
|  |  | |||
|  | @ -205,7 +205,6 @@ class ContributionsFragmentUnitTests { | |||
|         `when`(menu.findItem(anyInt())).thenReturn(menuItem) | ||||
|         `when`(menuItem.actionView).thenReturn(notification) | ||||
|         `when`(store.getBoolean(anyString(), anyBoolean())).thenReturn(true) | ||||
|         fragment.updateLimitedConnectionToggle(menu) | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|  |  | |||
|  | @ -137,20 +137,6 @@ class ContributionsListFragmentUnitTests { | |||
|         method.invoke(fragment, contribution) | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     @Throws(Exception::class) | ||||
|     fun testResumeUpload() { | ||||
|         Shadows.shadowOf(Looper.getMainLooper()).idle() | ||||
|         fragment.resumeUpload(contribution) | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     @Throws(Exception::class) | ||||
|     fun testPauseUpload() { | ||||
|         Shadows.shadowOf(Looper.getMainLooper()).idle() | ||||
|         fragment.pauseUpload(contribution) | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     @Throws(Exception::class) | ||||
|     fun testAddImageToWikipedia() { | ||||
|  | @ -165,20 +151,6 @@ class ContributionsListFragmentUnitTests { | |||
|         fragment.openMediaDetail(0, true) | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     @Throws(Exception::class) | ||||
|     fun testDeleteUpload() { | ||||
|         Shadows.shadowOf(Looper.getMainLooper()).idle() | ||||
|         fragment.deleteUpload(contribution) | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     @Throws(Exception::class) | ||||
|     fun testRetryUpload() { | ||||
|         Shadows.shadowOf(Looper.getMainLooper()).idle() | ||||
|         fragment.retryUpload(contribution) | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     @Throws(Exception::class) | ||||
|     fun testOnViewStateRestored() { | ||||
|  |  | |||
|  | @ -54,12 +54,4 @@ class ContributionsListPresenterTest { | |||
|             ); | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     fun testDeleteUpload() { | ||||
|         whenever(repository.deleteContributionFromDB(any<Contribution>())) | ||||
|             .thenReturn(Completable.complete()) | ||||
|         contributionsListPresenter.deleteUpload(mock(Contribution::class.java)) | ||||
|         verify(repository, times(1)) | ||||
|             .deleteContributionFromDB(ArgumentMatchers.any(Contribution::class.java)); | ||||
|     } | ||||
| } | ||||
|  | @ -8,6 +8,7 @@ import androidx.loader.content.CursorLoader | |||
| import androidx.loader.content.Loader | ||||
| import com.nhaarman.mockitokotlin2.verify | ||||
| import com.nhaarman.mockitokotlin2.whenever | ||||
| import fr.free.nrw.commons.repository.UploadRepository | ||||
| import io.reactivex.Completable | ||||
| import io.reactivex.schedulers.TestScheduler | ||||
| import org.junit.Before | ||||
|  | @ -24,6 +25,10 @@ import org.mockito.MockitoAnnotations | |||
| class ContributionsPresenterTest { | ||||
|     @Mock | ||||
|     internal lateinit var repository: ContributionsRepository | ||||
| 
 | ||||
|     @Mock | ||||
|     internal lateinit var uploadRepository: UploadRepository | ||||
| 
 | ||||
|     @Mock | ||||
|     internal lateinit var view: ContributionsContract.View | ||||
| 
 | ||||
|  | @ -37,9 +42,11 @@ class ContributionsPresenterTest { | |||
| 
 | ||||
|     lateinit var liveData: LiveData<List<Contribution>> | ||||
| 
 | ||||
|     @Rule @JvmField var instantTaskExecutorRule = InstantTaskExecutorRule() | ||||
|     @Rule | ||||
|     @JvmField | ||||
|     var instantTaskExecutorRule = InstantTaskExecutorRule() | ||||
| 
 | ||||
|     lateinit var scheduler : TestScheduler | ||||
|     lateinit var scheduler: TestScheduler | ||||
| 
 | ||||
|     /** | ||||
|      * initial setup | ||||
|  | @ -48,35 +55,23 @@ class ContributionsPresenterTest { | |||
|     @Throws(Exception::class) | ||||
|     fun setUp() { | ||||
|         MockitoAnnotations.initMocks(this) | ||||
|         scheduler=TestScheduler() | ||||
|         scheduler = TestScheduler() | ||||
|         cursor = Mockito.mock(Cursor::class.java) | ||||
|         contribution = Mockito.mock(Contribution::class.java) | ||||
|         contributionsPresenter = ContributionsPresenter(repository, scheduler) | ||||
|         contributionsPresenter = ContributionsPresenter(repository, uploadRepository, scheduler) | ||||
|         loader = Mockito.mock(CursorLoader::class.java) | ||||
|         contributionsPresenter.onAttachView(view) | ||||
|         liveData=MutableLiveData() | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Test presenter actions onDeleteContribution | ||||
|      */ | ||||
|     @Test | ||||
|     fun testDeleteContribution() { | ||||
|         whenever(repository.deleteContributionFromDB(ArgumentMatchers.any<Contribution>())) | ||||
|             .thenReturn(Completable.complete()) | ||||
|         contributionsPresenter.deleteUpload(contribution) | ||||
|         verify(repository).deleteContributionFromDB(contribution) | ||||
|         liveData = MutableLiveData() | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Test fetch contribution with filename | ||||
|      */ | ||||
|     @Test | ||||
|     fun testGetContributionWithFileName(){ | ||||
|     fun testGetContributionWithFileName() { | ||||
|         contributionsPresenter.getContributionsWithTitle("ashish") | ||||
|         verify(repository).getContributionWithFileName("ashish") | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| } | ||||
|  | @ -195,25 +195,6 @@ class MainActivityUnitTests { | |||
|         MainActivity.startYourself(mockContext) | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     @Throws(Exception::class) | ||||
|     fun testToggleLimitedConnectionModeCaseDefault() { | ||||
|         activity.toggleLimitedConnectionMode() | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     @Throws(Exception::class) | ||||
|     fun testToggleLimitedConnectionMode() { | ||||
|         Shadows.shadowOf(Looper.getMainLooper()).idle() | ||||
|         `when`( | ||||
|             defaultKvStore.getBoolean( | ||||
|                 CommonsApplication.IS_LIMITED_CONNECTION_MODE_ENABLED, false | ||||
|             ) | ||||
|         ) | ||||
|             .thenReturn(false) | ||||
|         activity.toggleLimitedConnectionMode() | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     @Throws(Exception::class) | ||||
|     fun testSetUpPager() { | ||||
|  |  | |||
|  | @ -15,19 +15,25 @@ import fr.free.nrw.commons.CommonsApplication.DEFAULT_EDIT_SUMMARY | |||
| import fr.free.nrw.commons.auth.csrf.CsrfTokenClient | ||||
| import fr.free.nrw.commons.contributions.ChunkInfo | ||||
| import fr.free.nrw.commons.contributions.Contribution | ||||
| import fr.free.nrw.commons.contributions.ContributionDao | ||||
| import fr.free.nrw.commons.upload.UploadClient.TimeProvider | ||||
| import fr.free.nrw.commons.wikidata.mwapi.MwException | ||||
| import fr.free.nrw.commons.wikidata.mwapi.MwServiceError | ||||
| import io.reactivex.Observable | ||||
| import junit.framework.TestCase.assertEquals | ||||
| import junit.framework.TestCase.assertSame | ||||
| import kotlinx.coroutines.runBlocking | ||||
| import okhttp3.MediaType.Companion.toMediaType | ||||
| import okhttp3.MultipartBody | ||||
| import okhttp3.RequestBody | ||||
| import okhttp3.RequestBody.Companion.toRequestBody | ||||
| import okio.Buffer | ||||
| import org.junit.Assert | ||||
| import org.junit.Before | ||||
| import org.junit.Ignore | ||||
| import org.junit.Test | ||||
| import org.junit.jupiter.api.assertThrows | ||||
| import org.junit.platform.commons.annotation.Testable | ||||
| import java.io.File | ||||
| import java.util.Date | ||||
| 
 | ||||
|  | @ -41,14 +47,24 @@ class UploadClientTest { | |||
|     private val pageContentsCreator = mock<PageContentsCreator>() | ||||
|     private val fileUtilsWrapper = mock<FileUtilsWrapper>() | ||||
|     private val gson = mock<Gson>() | ||||
|     private val contributionDao = mock<ContributionDao> { } | ||||
|     private val timeProvider = mock<TimeProvider>() | ||||
|     private val uploadClient = UploadClient(uploadInterface, csrfTokenClient, pageContentsCreator, fileUtilsWrapper, gson, timeProvider) | ||||
|     private val uploadClient = UploadClient( | ||||
|         uploadInterface, | ||||
|         csrfTokenClient, | ||||
|         pageContentsCreator, | ||||
|         fileUtilsWrapper, | ||||
|         gson, | ||||
|         timeProvider, | ||||
|         contributionDao | ||||
|     ) | ||||
| 
 | ||||
|     private val expectedChunkSize = 512 * 1024 | ||||
|     private val testToken = "test-token" | ||||
|     private val createdContent = "content" | ||||
|     private val filename = "test.jpg" | ||||
|     private val filekey = "the-key" | ||||
|     private val pageId = "page-id" | ||||
|     private val errorCode = "the-code" | ||||
|     private val uploadJson = Gson().fromJson("{\"foo\" = 1}", JsonObject::class.java) | ||||
| 
 | ||||
|  | @ -64,7 +80,15 @@ class UploadClientTest { | |||
|     @Test | ||||
|     fun testUploadFileFromStash_NoErrors() { | ||||
|         whenever(gson.fromJson(uploadJson, UploadResponse::class.java)).thenReturn(uploadResponse) | ||||
|         whenever(uploadInterface.uploadFileFromStash(testToken, createdContent, DEFAULT_EDIT_SUMMARY, filename, filekey)).thenReturn(Observable.just(uploadJson)) | ||||
|         whenever( | ||||
|             uploadInterface.uploadFileFromStash( | ||||
|                 testToken, | ||||
|                 createdContent, | ||||
|                 DEFAULT_EDIT_SUMMARY, | ||||
|                 filename, | ||||
|                 filekey | ||||
|             ) | ||||
|         ).thenReturn(Observable.just(uploadJson)) | ||||
| 
 | ||||
|         val result = uploadClient.uploadFileFromStash(contribution, filename, filekey).test() | ||||
| 
 | ||||
|  | @ -80,7 +104,15 @@ class UploadClientTest { | |||
| 
 | ||||
|         whenever(gson.fromJson(uploadJson, UploadResponse::class.java)).thenReturn(errorResponse) | ||||
|         whenever(gson.fromJson(uploadJson, MwException::class.java)).thenReturn(uploadException) | ||||
|         whenever(uploadInterface.uploadFileFromStash(testToken, createdContent, DEFAULT_EDIT_SUMMARY, filename, filekey)).thenReturn(Observable.just(uploadJson)) | ||||
|         whenever( | ||||
|             uploadInterface.uploadFileFromStash( | ||||
|                 testToken, | ||||
|                 createdContent, | ||||
|                 DEFAULT_EDIT_SUMMARY, | ||||
|                 filename, | ||||
|                 filekey | ||||
|             ) | ||||
|         ).thenReturn(Observable.just(uploadJson)) | ||||
| 
 | ||||
|         val result = uploadClient.uploadFileFromStash(contribution, filename, filekey).test() | ||||
| 
 | ||||
|  | @ -91,7 +123,15 @@ class UploadClientTest { | |||
|     @Test | ||||
|     fun testUploadFileFromStash_Failure() { | ||||
|         val exception = Exception("test") | ||||
|         whenever(uploadInterface.uploadFileFromStash(testToken, createdContent, DEFAULT_EDIT_SUMMARY, filename, filekey)) | ||||
|         whenever( | ||||
|             uploadInterface.uploadFileFromStash( | ||||
|                 testToken, | ||||
|                 createdContent, | ||||
|                 DEFAULT_EDIT_SUMMARY, | ||||
|                 filename, | ||||
|                 filekey | ||||
|             ) | ||||
|         ) | ||||
|             .thenReturn(Observable.error(exception)) | ||||
| 
 | ||||
|         val result = uploadClient.uploadFileFromStash(contribution, filename, filekey).test() | ||||
|  | @ -104,7 +144,8 @@ class UploadClientTest { | |||
|     fun testUploadChunkToStash_Success() { | ||||
|         val fileContent = "content" | ||||
|         val requestBody: RequestBody = fileContent.toRequestBody("text/plain".toMediaType()) | ||||
|         val countingRequestBody = CountingRequestBody(requestBody, mock(), 0, fileContent.length.toLong()) | ||||
|         val countingRequestBody = | ||||
|             CountingRequestBody(requestBody, mock(), 0, fileContent.length.toLong()) | ||||
| 
 | ||||
|         val filenameCaptor: KArgumentCaptor<RequestBody> = argumentCaptor<RequestBody>() | ||||
|         val totalFileSizeCaptor = argumentCaptor<RequestBody>() | ||||
|  | @ -113,12 +154,15 @@ class UploadClientTest { | |||
|         val tokenCaptor = argumentCaptor<RequestBody>() | ||||
|         val fileCaptor = argumentCaptor<MultipartBody.Part>() | ||||
| 
 | ||||
|         whenever(uploadInterface.uploadFileToStash( | ||||
|         whenever( | ||||
|             uploadInterface.uploadFileToStash( | ||||
|                 filenameCaptor.capture(), totalFileSizeCaptor.capture(), offsetCaptor.capture(), | ||||
|                 fileKeyCaptor.capture(), tokenCaptor.capture(), fileCaptor.capture() | ||||
|         )).thenReturn(Observable.just(uploadResponse)) | ||||
|             ) | ||||
|         ).thenReturn(Observable.just(uploadResponse)) | ||||
| 
 | ||||
|         val result = uploadClient.uploadChunkToStash(filename, 100, 10, filekey, countingRequestBody).test() | ||||
|         val result = | ||||
|             uploadClient.uploadChunkToStash(filename, 100, 10, filekey, countingRequestBody).test() | ||||
| 
 | ||||
|         result.assertNoErrors() | ||||
|         assertSame(uploadResult, result.values()[0]) | ||||
|  | @ -156,28 +200,18 @@ class UploadClientTest { | |||
|         assertEquals(StashUploadState.SUCCESS, stashResult.state) | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     fun uploadFileToStash_contributionIsUnpaused() { | ||||
|         whenever(contribution.isCompleted()).thenReturn(false) | ||||
|         whenever(contribution.fileKey).thenReturn(filekey) | ||||
|         whenever(fileUtilsWrapper.getMimeType(anyOrNull<File>())).thenReturn("image/png") | ||||
|         whenever(fileUtilsWrapper.getFileChunks(anyOrNull<File>(), eq(expectedChunkSize))).thenReturn(emptyList()) | ||||
| 
 | ||||
|         val result = uploadClient.uploadFileToStash(filename, contribution, mock()).test() | ||||
| 
 | ||||
|         result.assertNoErrors() | ||||
|         verify(contribution, times(1)).unpause() | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     fun uploadFileToStash_returnsFailureIfNothingToUpload() { | ||||
|         val tempFile = File.createTempFile("tempFile", ".tmp") | ||||
|         tempFile.deleteOnExit() | ||||
|         whenever(contribution.isCompleted()).thenReturn(false) | ||||
|         whenever(contribution.fileKey).thenReturn(filekey) | ||||
|         whenever(contribution.pageId).thenReturn(pageId) | ||||
|         whenever(contributionDao.getContribution(pageId)).thenReturn(contribution) | ||||
|         whenever(contribution.localUriPath).thenReturn(tempFile) | ||||
|         whenever(fileUtilsWrapper.getMimeType(anyOrNull<File>())).thenReturn("image/png") | ||||
|         whenever(fileUtilsWrapper.getFileChunks(anyOrNull<File>(), eq(expectedChunkSize))).thenReturn(emptyList()) | ||||
| 
 | ||||
|         val result = uploadClient.uploadFileToStash(filename, contribution, mock()).test() | ||||
| 
 | ||||
|         val result = uploadClient.uploadFileToStash(filename, contribution, mock() ).test() | ||||
|         result.assertNoErrors() | ||||
|         assertEquals(StashUploadState.FAILED, result.values()[0].state) | ||||
|     } | ||||
|  | @ -188,10 +222,26 @@ class UploadClientTest { | |||
|         whenever(mockFile.length()).thenReturn(1) | ||||
|         whenever(contribution.localUriPath).thenReturn(mockFile) | ||||
|         whenever(contribution.isCompleted()).thenReturn(false) | ||||
|         whenever(contribution.pageId).thenReturn(pageId) | ||||
|         whenever(contributionDao.getContribution(pageId)).thenReturn(contribution) | ||||
|         whenever(contribution.fileKey).thenReturn(filekey) | ||||
|         whenever(fileUtilsWrapper.getMimeType(anyOrNull<File>())).thenReturn("image/png") | ||||
|         whenever(fileUtilsWrapper.getFileChunks(anyOrNull<File>(), eq(expectedChunkSize))).thenReturn(listOf(mockFile)) | ||||
|         whenever(uploadInterface.uploadFileToStash(any(), any(), any(), any(), any(), any())).thenReturn(Observable.just(uploadResponse)) | ||||
|         whenever( | ||||
|             fileUtilsWrapper.getFileChunks( | ||||
|                 anyOrNull<File>(), | ||||
|                 eq(expectedChunkSize) | ||||
|             ) | ||||
|         ).thenReturn(listOf(mockFile)) | ||||
|         whenever( | ||||
|             uploadInterface.uploadFileToStash( | ||||
|                 any(), | ||||
|                 any(), | ||||
|                 any(), | ||||
|                 any(), | ||||
|                 any(), | ||||
|                 any() | ||||
|             ) | ||||
|         ).thenReturn(Observable.just(uploadResponse)) | ||||
| 
 | ||||
|         val result = uploadClient.uploadFileToStash(filename, contribution, mock()).test() | ||||
| 
 | ||||
|  | @ -215,12 +265,23 @@ class UploadClientTest { | |||
|         whenever(contribution.dateModified).thenReturn(Date(100)) | ||||
|         whenever(timeProvider.currentTimeMillis()).thenReturn(200) | ||||
|         whenever(contribution.fileKey).thenReturn(filekey) | ||||
|         whenever(contribution.pageId).thenReturn(pageId) | ||||
|         whenever(contributionDao.getContribution(pageId)).thenReturn(contribution) | ||||
| 
 | ||||
|         whenever(fileUtilsWrapper.getMimeType(anyOrNull<File>())).thenReturn("image/png") | ||||
|         whenever(fileUtilsWrapper.getFileChunks(anyOrNull<File>(), eq(expectedChunkSize))).thenReturn(listOf(mockFile)) | ||||
|         whenever( | ||||
|             fileUtilsWrapper.getFileChunks( | ||||
|                 anyOrNull<File>(), | ||||
|                 eq(expectedChunkSize) | ||||
|             ) | ||||
|         ).thenReturn(listOf(mockFile)) | ||||
| 
 | ||||
|         whenever(uploadInterface.uploadFileToStash(anyOrNull(), anyOrNull(), anyOrNull(), | ||||
|             anyOrNull(), anyOrNull(), anyOrNull())).thenReturn(Observable.just(uploadResponse)) | ||||
|         whenever( | ||||
|             uploadInterface.uploadFileToStash( | ||||
|                 anyOrNull(), anyOrNull(), anyOrNull(), | ||||
|                 anyOrNull(), anyOrNull(), anyOrNull() | ||||
|             ) | ||||
|         ).thenReturn(Observable.just(uploadResponse)) | ||||
| 
 | ||||
|         val result = uploadClient.uploadFileToStash(filename, contribution, mock()).test() | ||||
| 
 | ||||
|  |  | |||
|  | @ -8,6 +8,7 @@ import fr.free.nrw.commons.Media | |||
| import fr.free.nrw.commons.contributions.Contribution | ||||
| import fr.free.nrw.commons.kvstore.JsonKvStore | ||||
| import org.junit.Before | ||||
| import org.junit.Ignore | ||||
| import org.junit.Test | ||||
| import org.mockito.InjectMocks | ||||
| import org.mockito.Mock | ||||
|  | @ -32,6 +33,7 @@ class UploadControllerTest { | |||
|         MockitoAnnotations.openMocks(this) | ||||
|     } | ||||
| 
 | ||||
|     @Ignore | ||||
|     @Test | ||||
|     fun startUpload() { | ||||
|         val contribution = mock(Contribution::class.java) | ||||
|  |  | |||
|  | @ -6,6 +6,7 @@ import fr.free.nrw.commons.kvstore.JsonKvStore | |||
| import fr.free.nrw.commons.upload.structure.depictions.DepictedItem | ||||
| import media | ||||
| import org.junit.Before | ||||
| import org.junit.Ignore | ||||
| import org.junit.Test | ||||
| import org.mockito.Mockito.mock | ||||
| import org.mockito.MockitoAnnotations | ||||
|  | @ -28,6 +29,7 @@ class UploadModelUnitTest { | |||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     @Ignore | ||||
|     @Test | ||||
|     fun `Test onDepictItemClicked when DepictedItem is selected`(){ | ||||
|         uploadModel.onDepictItemClicked( | ||||
|  | @ -42,6 +44,7 @@ class UploadModelUnitTest { | |||
|             ), media(filename = "File:Example.jpg")) | ||||
|     } | ||||
| 
 | ||||
|     @Ignore | ||||
|     @Test | ||||
|     fun `Test onDepictItemClicked when DepictedItem is not selected`(){ | ||||
|         uploadModel.onDepictItemClicked( | ||||
|  | @ -57,6 +60,7 @@ class UploadModelUnitTest { | |||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     @Ignore | ||||
|     @Test | ||||
|     fun `Test onDepictItemClicked when DepictedItem is not selected and not included in media`(){ | ||||
|         uploadModel.onDepictItemClicked( | ||||
|  | @ -72,6 +76,7 @@ class UploadModelUnitTest { | |||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     @Ignore | ||||
|     @Test | ||||
|     fun `Test onDepictItemClicked when media is null and DepictedItem is not selected`(){ | ||||
|         uploadModel.onDepictItemClicked( | ||||
|  | @ -86,6 +91,7 @@ class UploadModelUnitTest { | |||
|             ), null) | ||||
|     } | ||||
| 
 | ||||
|     @Ignore | ||||
|     @Test | ||||
|     fun `Test onDepictItemClicked when media is not null and DepictedItem is selected`(){ | ||||
|         uploadModel.onDepictItemClicked( | ||||
|  | @ -100,6 +106,7 @@ class UploadModelUnitTest { | |||
|             ), media(filename = "File:Example.jpg")) | ||||
|     } | ||||
| 
 | ||||
|     @Ignore | ||||
|     @Test | ||||
|     fun `Test onDepictItemClicked when media is null and DepictedItem is selected`(){ | ||||
|         uploadModel.onDepictItemClicked( | ||||
|  | @ -114,11 +121,13 @@ class UploadModelUnitTest { | |||
|             ), null) | ||||
|     } | ||||
| 
 | ||||
|     @Ignore | ||||
|     @Test | ||||
|     fun testGetSelectedExistingDepictions(){ | ||||
|         uploadModel.selectedExistingDepictions | ||||
|     } | ||||
| 
 | ||||
|     @Ignore | ||||
|     @Test | ||||
|     fun testSetSelectedExistingDepictions(){ | ||||
|         uploadModel.selectedExistingDepictions = listOf("") | ||||
|  |  | |||
|  | @ -9,6 +9,7 @@ import fr.free.nrw.commons.repository.UploadRepository | |||
| import fr.free.nrw.commons.upload.ImageCoordinates | ||||
| import io.reactivex.Observable | ||||
| import org.junit.Before | ||||
| import org.junit.Ignore | ||||
| import org.junit.Test | ||||
| import org.mockito.ArgumentMatchers | ||||
| import org.mockito.InjectMocks | ||||
|  | @ -68,6 +69,7 @@ class UploadPresenterTest { | |||
|     /** | ||||
|      * unit test case for method UploadPresenter.handleSubmit | ||||
|      */ | ||||
|     @Ignore | ||||
|     @Test | ||||
|     fun handleSubmitTestUserLoggedIn() { | ||||
|         `when`(view.isLoggedIn).thenReturn(true) | ||||
|  | @ -78,6 +80,7 @@ class UploadPresenterTest { | |||
|         verify(repository).buildContributions() | ||||
|     } | ||||
| 
 | ||||
|     @Ignore | ||||
|     @Test | ||||
|     fun handleSubmitImagesNoLocationWithConsecutiveNoLocationUploads() { | ||||
|         `when`(imageCoords.imageCoordsExists).thenReturn(false) | ||||
|  | @ -102,6 +105,7 @@ class UploadPresenterTest { | |||
|         verify(view).showAlertDialog(ArgumentMatchers.anyInt(), ArgumentMatchers.any<Runnable>()) | ||||
|     } | ||||
| 
 | ||||
|     @Ignore | ||||
|     @Test | ||||
|     fun handleSubmitImagesWithLocationWithConsecutiveNoLocationUploads() { | ||||
|         `when`( | ||||
|  | @ -117,6 +121,7 @@ class UploadPresenterTest { | |||
|             .showAlertDialog(ArgumentMatchers.anyInt(), ArgumentMatchers.any<Runnable>()) | ||||
|     } | ||||
| 
 | ||||
|     @Ignore | ||||
|     @Test | ||||
|     fun handleSubmitTestUserLoggedInAndLimitedConnectionOn() { | ||||
|         `when`( | ||||
|  | @ -136,6 +141,7 @@ class UploadPresenterTest { | |||
|     /** | ||||
|      * unit test case for method UploadPresenter.handleSubmit | ||||
|      */ | ||||
|     @Ignore | ||||
|     @Test | ||||
|     fun handleSubmitTestUserNotLoggedIn() { | ||||
|         `when`(view.isLoggedIn).thenReturn(false) | ||||
|  | @ -152,6 +158,7 @@ class UploadPresenterTest { | |||
|     /** | ||||
|      * Test which asserts If the next fragment to be shown is not one of the MediaDetailsFragment, lets hide the top card | ||||
|      */ | ||||
|     @Ignore | ||||
|     @Test | ||||
|     fun hideTopCardWhenReachedTheLastFile(){ | ||||
|         deletePictureBaseTest() | ||||
|  | @ -163,6 +170,7 @@ class UploadPresenterTest { | |||
|     /** | ||||
|      * Test media deletion during single upload | ||||
|      */ | ||||
|     @Ignore | ||||
|     @Test | ||||
|     fun testDeleteWhenSingleUpload(){ | ||||
|         deletePictureBaseTest() | ||||
|  | @ -176,6 +184,7 @@ class UploadPresenterTest { | |||
|     /** | ||||
|      * Test media deletion during multiple upload | ||||
|      */ | ||||
|     @Ignore | ||||
|     @Test | ||||
|     fun testDeleteWhenMultipleFilesUpload(){ | ||||
|         deletePictureBaseTest() | ||||
|  |  | |||
|  | @ -17,6 +17,7 @@ import fr.free.nrw.commons.upload.structure.depictions.DepictedItem | |||
| import io.reactivex.Completable | ||||
| import io.reactivex.Single | ||||
| import org.junit.Before | ||||
| import org.junit.Ignore | ||||
| import org.junit.Test | ||||
| import org.junit.jupiter.api.Assertions.assertEquals | ||||
| import org.mockito.Mock | ||||
|  | @ -199,7 +200,6 @@ class UploadRepositoryUnitTest { | |||
|         ) | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     @Test | ||||
|     fun testDeletePicture() { | ||||
|         assertEquals(repository.deletePicture(""), uploadModel.deletePicture("")) | ||||
|  |  | |||
|  | @ -6,6 +6,7 @@ import entity | |||
| import entityId | ||||
| import fr.free.nrw.commons.wikidata.WikidataProperties | ||||
| import org.junit.Assert | ||||
| import org.junit.Ignore | ||||
| import org.junit.Test | ||||
| import place | ||||
| import snak | ||||
|  |  | |||
|  | @ -4,6 +4,7 @@ import com.nhaarman.mockitokotlin2.mock | |||
| import fr.free.nrw.commons.upload.FileUtils | ||||
| import fr.free.nrw.commons.upload.FileUtilsWrapper | ||||
| import org.junit.Assert.assertEquals | ||||
| import org.junit.Ignore | ||||
| import org.junit.Test | ||||
| import java.io.* | ||||
| 
 | ||||
|  |  | |||
|  | @ -13,6 +13,7 @@ import junit.framework.TestCase.assertEquals | |||
| import junit.framework.TestCase.assertSame | ||||
| import junit.framework.TestCase.assertTrue | ||||
| import org.junit.Before | ||||
| import org.junit.Ignore | ||||
| import org.junit.Test | ||||
| import org.mockito.Mockito.mock | ||||
| 
 | ||||
|  |  | |||
|  | @ -17,6 +17,7 @@ import org.mockito.MockitoAnnotations | |||
| import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse | ||||
| import fr.free.nrw.commons.wikidata.mwapi.MwQueryResult | ||||
| import fr.free.nrw.commons.wikidata.model.Statement_partial | ||||
| import org.junit.Ignore | ||||
| 
 | ||||
| class WikidataClientTest { | ||||
| 
 | ||||
|  |  | |||
|  | @ -20,7 +20,6 @@ allprojects { | |||
|         gradlePluginPortal() // potential jcenter() replacement | ||||
|         maven { url "https://jitpack.io" } | ||||
|         maven { url "https://maven.google.com" } | ||||
|         jcenter() | ||||
|     } | ||||
| } | ||||
| subprojects{ | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Nicolas Raoul
						Nicolas Raoul