mirror of
				https://github.com/commons-app/apps-android-commons.git
				synced 2025-10-31 14:53:59 +01:00 
			
		
		
		
	Merge branch 'main' into upgrade
This commit is contained in:
		
						commit
						c8e9232508
					
				
					 71 changed files with 2728 additions and 967 deletions
				
			
		|  | @ -195,36 +195,4 @@ class MainActivityTest { | ||||||
|         Espresso.pressBack() |         Espresso.pressBack() | ||||||
|         UITestHelper.sleep(1000) |         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) |  | ||||||
|             ) |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
| } | } | ||||||
|  | @ -20,7 +20,7 @@ | ||||||
|   <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" |   <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" | ||||||
|     android:minSdkVersion="33"/> |     android:minSdkVersion="33"/> | ||||||
|   <uses-permission android:name="com.google.android.apps.photos.permission.GOOGLE_PHOTOS" /> |   <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.FOREGROUND_SERVICE" /> | ||||||
|   <uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION"/> |   <uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION"/> | ||||||
|   <uses-permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED" |   <uses-permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED" | ||||||
|  | @ -28,58 +28,57 @@ | ||||||
|   <uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" /> |   <uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" /> | ||||||
| 
 | 
 | ||||||
|   <queries> |   <queries> | ||||||
|  | 
 | ||||||
|     <!-- Browser --> |     <!-- Browser --> | ||||||
|     <intent> |     <intent> | ||||||
|       <action android:name="android.intent.action.VIEW" /> |       <action android:name="android.intent.action.VIEW" /> | ||||||
|  | 
 | ||||||
|       <category android:name="android.intent.category.BROWSABLE" /> |       <category android:name="android.intent.category.BROWSABLE" /> | ||||||
|  | 
 | ||||||
|       <data android:scheme="https" /> |       <data android:scheme="https" /> | ||||||
|     </intent> |     </intent> | ||||||
|     <!-- Google Maps --> |     <!-- Google Maps --> | ||||||
|     <package android:name="com.google.android.apps.maps" /> |     <package android:name="com.google.android.apps.maps" /> | ||||||
|   </queries> |   </queries> <!-- Needed only if your app targets Android 5.0 (API level 21) or higher. --> | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|   <!-- Needed only if your app targets Android 5.0 (API level 21) or higher. --> |  | ||||||
|   <uses-feature android:name="android.hardware.location.gps" /> |   <uses-feature android:name="android.hardware.location.gps" /> | ||||||
| 
 | 
 | ||||||
|   <application |   <application | ||||||
|     android:name=".CommonsApplication" |     android:name=".CommonsApplication" | ||||||
|  |     android:appComponentFactory="commons" | ||||||
|     android:icon="@mipmap/ic_launcher" |     android:icon="@mipmap/ic_launcher" | ||||||
|     android:label="@string/app_name" |     android:label="@string/app_name" | ||||||
|     android:theme="@style/LightAppTheme" |  | ||||||
|     android:largeHeap="true" |     android:largeHeap="true" | ||||||
|  |     android:requestLegacyExternalStorage="true" | ||||||
|     android:supportsRtl="true" |     android:supportsRtl="true" | ||||||
|     tools:replace="android:appComponentFactory" |     android:theme="@style/LightAppTheme" | ||||||
|     android:appComponentFactory="commons" |     tools:ignore="GoogleAppIndexingWarning" | ||||||
|     android:requestLegacyExternalStorage = "true" |     tools:replace="android:appComponentFactory"> | ||||||
|     tools:ignore="GoogleAppIndexingWarning"> |  | ||||||
| 
 |  | ||||||
|     <activity |     <activity | ||||||
|       android:name=".nearby.WikidataFeedback" |       android:name=".nearby.WikidataFeedback" | ||||||
|       android:exported="false" /> |       android:exported="false" /> | ||||||
| 
 |  | ||||||
|     <activity |     <activity | ||||||
|       android:theme="@style/EditActivityTheme" |       android:name=".upload.UploadProgressActivity" | ||||||
|       android:name=".description.DescriptionEditActivity" |  | ||||||
|       android:exported="false" /> |       android:exported="false" /> | ||||||
| 
 |     <activity | ||||||
|  |       android:name=".description.DescriptionEditActivity" | ||||||
|  |       android:exported="true" | ||||||
|  |       android:theme="@style/EditActivityTheme" /> | ||||||
|     <activity |     <activity | ||||||
|       android:name=".edit.EditActivity" |       android:name=".edit.EditActivity" | ||||||
|       android:exported="false" /> |       android:exported="false" /> | ||||||
| 
 |     <activity | ||||||
|     <activity android:name="org.acra.dialog.CrashReportDialog" |       android:name="org.acra.dialog.CrashReportDialog" | ||||||
|       android:process=":acra" |  | ||||||
|       android:launchMode="singleInstance" |  | ||||||
|       android:excludeFromRecents="true" |       android:excludeFromRecents="true" | ||||||
|       android:finishOnTaskLaunch="true" /> |       android:finishOnTaskLaunch="true" | ||||||
| 
 |       android:launchMode="singleInstance" | ||||||
|  |       android:process=":acra" /> | ||||||
|     <activity |     <activity | ||||||
|       android:name=".media.ZoomableActivity" |       android:name=".media.ZoomableActivity" | ||||||
|       android:label="Zoomable Activity" |  | ||||||
|       android:configChanges="screenSize|keyboard|orientation" |       android:configChanges="screenSize|keyboard|orientation" | ||||||
|  |       android:label="Zoomable Activity" | ||||||
|       android:parentActivityName=".customselector.ui.selector.CustomSelectorActivity" /> |       android:parentActivityName=".customselector.ui.selector.CustomSelectorActivity" /> | ||||||
| 
 |     <activity | ||||||
|     <activity android:name=".auth.LoginActivity" |       android:name=".auth.LoginActivity" | ||||||
|       android:exported="true"> |       android:exported="true"> | ||||||
|       <intent-filter> |       <intent-filter> | ||||||
|         <category android:name="android.intent.category.LAUNCHER" /> |         <category android:name="android.intent.category.LAUNCHER" /> | ||||||
|  | @ -87,21 +86,19 @@ | ||||||
|         <action android:name="android.intent.action.MAIN" /> |         <action android:name="android.intent.action.MAIN" /> | ||||||
|       </intent-filter> |       </intent-filter> | ||||||
| 
 | 
 | ||||||
|       <meta-data android:name="android.app.shortcuts" |       <meta-data | ||||||
|  |         android:name="android.app.shortcuts" | ||||||
|         android:resource="@xml/shortcuts" /> |         android:resource="@xml/shortcuts" /> | ||||||
| 
 |  | ||||||
|     </activity> |     </activity> | ||||||
|     <activity android:name=".WelcomeActivity" /> |     <activity android:name=".WelcomeActivity" /> | ||||||
| 
 |  | ||||||
|     <activity |     <activity | ||||||
|       android:hardwareAccelerated="false" |  | ||||||
|       android:name=".upload.UploadActivity" |       android:name=".upload.UploadActivity" | ||||||
|       android:exported="true" |  | ||||||
|       android:configChanges="orientation|screenSize|keyboard" |       android:configChanges="orientation|screenSize|keyboard" | ||||||
|  |       android:exported="true" | ||||||
|  |       android:hardwareAccelerated="false" | ||||||
|       android:icon="@mipmap/ic_launcher" |       android:icon="@mipmap/ic_launcher" | ||||||
|       android:label="@string/app_name" |       android:label="@string/app_name" | ||||||
|       android:windowSoftInputMode="adjustResize" |       android:windowSoftInputMode="adjustResize"> | ||||||
|       > |  | ||||||
|       <intent-filter android:label="@string/intent_share_upload_label"> |       <intent-filter android:label="@string/intent_share_upload_label"> | ||||||
|         <action android:name="android.intent.action.SEND" /> |         <action android:name="android.intent.action.SEND" /> | ||||||
| 
 | 
 | ||||||
|  | @ -121,9 +118,9 @@ | ||||||
|     </activity> |     </activity> | ||||||
|     <activity |     <activity | ||||||
|       android:name=".contributions.MainActivity" |       android:name=".contributions.MainActivity" | ||||||
|  |       android:configChanges="screenSize|keyboard|orientation" | ||||||
|       android:icon="@mipmap/ic_launcher" |       android:icon="@mipmap/ic_launcher" | ||||||
|       android:label="@string/app_name" |       android:label="@string/app_name" /> | ||||||
|       android:configChanges="screenSize|keyboard|orientation" /> |  | ||||||
|     <activity |     <activity | ||||||
|       android:name=".settings.SettingsActivity" |       android:name=".settings.SettingsActivity" | ||||||
|       android:label="@string/title_activity_settings" /> |       android:label="@string/title_activity_settings" /> | ||||||
|  | @ -131,57 +128,47 @@ | ||||||
|       android:name=".AboutActivity" |       android:name=".AboutActivity" | ||||||
|       android:label="@string/title_activity_about" |       android:label="@string/title_activity_about" | ||||||
|       android:parentActivityName=".contributions.MainActivity" /> |       android:parentActivityName=".contributions.MainActivity" /> | ||||||
| 
 |  | ||||||
|     <activity |     <activity | ||||||
|       android:name=".auth.SignupActivity" |       android:name=".auth.SignupActivity" | ||||||
|       android:configChanges="orientation|screenLayout|screenSize" |       android:configChanges="orientation|screenLayout|screenSize" | ||||||
|       android:label="@string/title_activity_signup" /> |       android:label="@string/title_activity_signup" /> | ||||||
| 
 |  | ||||||
|     <activity |     <activity | ||||||
|       android:name=".notification.NotificationActivity" |       android:name=".notification.NotificationActivity" | ||||||
|       android:label="@string/navigation_item_notification" /> |       android:label="@string/navigation_item_notification" /> | ||||||
| 
 |     <activity | ||||||
|     <activity android:name=".quiz.QuizActivity" |       android:name=".quiz.QuizActivity" | ||||||
|       android:label="@string/quiz"/> |       android:label="@string/quiz" /> | ||||||
| 
 |     <activity | ||||||
|     <activity android:name=".quiz.QuizResultActivity" |       android:name=".quiz.QuizResultActivity" | ||||||
|       android:label="@string/result"/> |       android:label="@string/result" /> | ||||||
| 
 |  | ||||||
|     <activity |     <activity | ||||||
|       android:name=".customselector.ui.selector.CustomSelectorActivity" |       android:name=".customselector.ui.selector.CustomSelectorActivity" | ||||||
|       android:label="@string/title_activity_custom_selector" |  | ||||||
|       android:configChanges="screenSize|keyboard|orientation" |       android:configChanges="screenSize|keyboard|orientation" | ||||||
|  |       android:label="@string/title_activity_custom_selector" | ||||||
|       android:parentActivityName=".contributions.MainActivity" /> |       android:parentActivityName=".contributions.MainActivity" /> | ||||||
| 
 |  | ||||||
|     <activity |     <activity | ||||||
|       android:name=".category.CategoryDetailsActivity" |       android:name=".category.CategoryDetailsActivity" | ||||||
|       android:label="@string/title_activity_featured_images" |  | ||||||
|       android:configChanges="screenSize|keyboard|orientation" |       android:configChanges="screenSize|keyboard|orientation" | ||||||
|  |       android:label="@string/title_activity_featured_images" | ||||||
|       android:parentActivityName=".contributions.MainActivity" /> |       android:parentActivityName=".contributions.MainActivity" /> | ||||||
| 
 |  | ||||||
|     <activity |     <activity | ||||||
|       android:name=".explore.depictions.WikidataItemDetailsActivity" |       android:name=".explore.depictions.WikidataItemDetailsActivity" | ||||||
|       android:label="@string/title_activity_featured_images" |  | ||||||
|       android:configChanges="screenSize|keyboard|orientation" |       android:configChanges="screenSize|keyboard|orientation" | ||||||
|  |       android:label="@string/title_activity_featured_images" | ||||||
|       android:parentActivityName=".contributions.MainActivity" /> |       android:parentActivityName=".contributions.MainActivity" /> | ||||||
| 
 |  | ||||||
|     <activity |     <activity | ||||||
|       android:name=".explore.SearchActivity" |       android:name=".explore.SearchActivity" | ||||||
|  |       android:configChanges="orientation|keyboardHidden|screenSize" | ||||||
|       android:label="@string/title_activity_search" |       android:label="@string/title_activity_search" | ||||||
|       android:launchMode="singleTop" |       android:launchMode="singleTop" | ||||||
|       android:configChanges="orientation|keyboardHidden|screenSize" |       android:parentActivityName=".contributions.MainActivity" /> | ||||||
|       android:parentActivityName=".contributions.MainActivity" |  | ||||||
|       /> |  | ||||||
| 
 |  | ||||||
|     <activity |     <activity | ||||||
|       android:name=".profile.ProfileActivity" |       android:name=".profile.ProfileActivity" | ||||||
|       android:configChanges="orientation|screenSize|keyboard" |       android:configChanges="orientation|screenSize|keyboard" | ||||||
|       android:label="@string/Profile" /> |       android:label="@string/Profile" /> | ||||||
| 
 |  | ||||||
|     <activity |     <activity | ||||||
|       android:name=".review.ReviewActivity" |       android:name=".review.ReviewActivity" | ||||||
|       android:label="@string/title_activity_review" /> |       android:label="@string/title_activity_review" /> | ||||||
| 
 |  | ||||||
|     <activity |     <activity | ||||||
|       android:name=".LocationPicker.LocationPickerActivity" |       android:name=".LocationPicker.LocationPickerActivity" | ||||||
|       android:label="Location Picker" /> |       android:label="Location Picker" /> | ||||||
|  | @ -193,11 +180,11 @@ | ||||||
|       <intent-filter> |       <intent-filter> | ||||||
|         <action android:name="android.accounts.AccountAuthenticator" /> |         <action android:name="android.accounts.AccountAuthenticator" /> | ||||||
|       </intent-filter> |       </intent-filter> | ||||||
|  | 
 | ||||||
|       <meta-data |       <meta-data | ||||||
|         android:name="android.accounts.AccountAuthenticator" |         android:name="android.accounts.AccountAuthenticator" | ||||||
|         android:resource="@xml/authenticator" /> |         android:resource="@xml/authenticator" /> | ||||||
|     </service> |     </service> | ||||||
| 
 |  | ||||||
|     <service |     <service | ||||||
|       android:name="org.acra.sender.SenderService" |       android:name="org.acra.sender.SenderService" | ||||||
|       android:exported="false" |       android:exported="false" | ||||||
|  | @ -216,42 +203,36 @@ | ||||||
|         android:name="android.support.FILE_PROVIDER_PATHS" |         android:name="android.support.FILE_PROVIDER_PATHS" | ||||||
|         android:resource="@xml/provider_paths" /> |         android:resource="@xml/provider_paths" /> | ||||||
|     </provider> |     </provider> | ||||||
| 
 |  | ||||||
|     <provider |     <provider | ||||||
|       android:name=".category.CategoryContentProvider" |       android:name=".category.CategoryContentProvider" | ||||||
|       android:authorities="${applicationId}.categories.contentprovider" |       android:authorities="${applicationId}.categories.contentprovider" | ||||||
|       android:exported="false" |       android:exported="false" | ||||||
|       android:label="@string/provider_categories" |       android:label="@string/provider_categories" | ||||||
|       android:syncable="false" /> |       android:syncable="false" /> | ||||||
| 
 |  | ||||||
|     <provider |     <provider | ||||||
|       android:name=".explore.recentsearches.RecentSearchesContentProvider" |       android:name=".explore.recentsearches.RecentSearchesContentProvider" | ||||||
|       android:authorities="${applicationId}.explore.recentsearches.contentprovider" |       android:authorities="${applicationId}.explore.recentsearches.contentprovider" | ||||||
|       android:exported="false" |       android:exported="false" | ||||||
|       android:label="@string/provider_searches" |       android:label="@string/provider_searches" | ||||||
|       android:syncable="false" /> |       android:syncable="false" /> | ||||||
| 
 |  | ||||||
|     <provider |     <provider | ||||||
|       android:name=".recentlanguages.RecentLanguagesContentProvider" |       android:name=".recentlanguages.RecentLanguagesContentProvider" | ||||||
|       android:authorities="${applicationId}.recentlanguages.contentprovider" |       android:authorities="${applicationId}.recentlanguages.contentprovider" | ||||||
|       android:exported="false" |       android:exported="false" | ||||||
|       android:label="@string/provider_recent_languages" |       android:label="@string/provider_recent_languages" | ||||||
|       android:syncable="false" /> |       android:syncable="false" /> | ||||||
| 
 |  | ||||||
|     <provider |     <provider | ||||||
|       android:name=".bookmarks.pictures.BookmarkPicturesContentProvider" |       android:name=".bookmarks.pictures.BookmarkPicturesContentProvider" | ||||||
|       android:authorities="${applicationId}.bookmarks.contentprovider" |       android:authorities="${applicationId}.bookmarks.contentprovider" | ||||||
|       android:exported="false" |       android:exported="false" | ||||||
|       android:label="@string/provider_bookmarks" |       android:label="@string/provider_bookmarks" | ||||||
|       android:syncable="false" /> |       android:syncable="false" /> | ||||||
| 
 |  | ||||||
|     <provider |     <provider | ||||||
|       android:name=".bookmarks.locations.BookmarkLocationsContentProvider" |       android:name=".bookmarks.locations.BookmarkLocationsContentProvider" | ||||||
|       android:authorities="${applicationId}.bookmarks.locations.contentprovider" |       android:authorities="${applicationId}.bookmarks.locations.contentprovider" | ||||||
|       android:exported="false" |       android:exported="false" | ||||||
|       android:label="@string/provider_bookmarks_location" |       android:label="@string/provider_bookmarks_location" | ||||||
|       android:syncable="false" /> |       android:syncable="false" /> | ||||||
| 
 |  | ||||||
|     <provider |     <provider | ||||||
|       android:name=".bookmarks.items.BookmarkItemsContentProvider" |       android:name=".bookmarks.items.BookmarkItemsContentProvider" | ||||||
|       android:authorities="${applicationId}.bookmarks.items.contentprovider" |       android:authorities="${applicationId}.bookmarks.items.contentprovider" | ||||||
|  | @ -259,7 +240,8 @@ | ||||||
|       android:label="@string/provider_bookmarks_location" |       android:label="@string/provider_bookmarks_location" | ||||||
|       android:syncable="false" /> |       android:syncable="false" /> | ||||||
| 
 | 
 | ||||||
|     <receiver android:name=".widget.PicOfDayAppWidget" |     <receiver | ||||||
|  |       android:name=".widget.PicOfDayAppWidget" | ||||||
|       android:exported="true"> |       android:exported="true"> | ||||||
|       <intent-filter> |       <intent-filter> | ||||||
|         <action android:name="android.appwidget.action.APPWIDGET_UPDATE" /> |         <action android:name="android.appwidget.action.APPWIDGET_UPDATE" /> | ||||||
|  | @ -270,8 +252,9 @@ | ||||||
|         android:resource="@xml/pic_of_day_app_widget_info" /> |         android:resource="@xml/pic_of_day_app_widget_info" /> | ||||||
|     </receiver> |     </receiver> | ||||||
| 
 | 
 | ||||||
|     <uses-library android:name="org.apache.http.legacy" android:required="false" /> |     <uses-library | ||||||
| 
 |       android:name="org.apache.http.legacy" | ||||||
|  |       android:required="false" /> | ||||||
|   </application> |   </application> | ||||||
| 
 | 
 | ||||||
| </manifest> | </manifest> | ||||||
|  | @ -142,15 +142,7 @@ public class CommonsApplication extends MultiDexApplication { | ||||||
|     @Inject |     @Inject | ||||||
|     ContributionDao contributionDao; |     ContributionDao contributionDao; | ||||||
| 
 | 
 | ||||||
|     /** |     public static Boolean isPaused = false; | ||||||
|      * 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<>(); |  | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Used to declare and initialize various components and dependencies |      * Used to declare and initialize various components and dependencies | ||||||
|  |  | ||||||
|  | @ -28,6 +28,7 @@ data class Contribution constructor( | ||||||
|     var dateCreatedSource: String? = null, |     var dateCreatedSource: String? = null, | ||||||
|     var wikidataPlace: WikidataPlace? = null, |     var wikidataPlace: WikidataPlace? = null, | ||||||
|     var chunkInfo: ChunkInfo? = null, |     var chunkInfo: ChunkInfo? = null, | ||||||
|  |     var errorInfo: String? = null, | ||||||
|     /** |     /** | ||||||
|      * @return array list of entityids for the depictions |      * @return array list of entityids for the depictions | ||||||
|      */ |      */ | ||||||
|  | @ -42,6 +43,7 @@ data class Contribution constructor( | ||||||
|     var dateCreated: Date? = null, |     var dateCreated: Date? = null, | ||||||
|     var dateCreatedString: String? = null, |     var dateCreatedString: String? = null, | ||||||
|     var dateModified: Date? = null, |     var dateModified: Date? = null, | ||||||
|  |     var dateUploadStarted: Date? = null, | ||||||
|     var hasInvalidLocation : Int =  0, |     var hasInvalidLocation : Int =  0, | ||||||
|     var contentUri: Uri? = null, |     var contentUri: Uri? = null, | ||||||
|     var countryCode : String? = null, |     var countryCode : String? = null, | ||||||
|  | @ -99,7 +101,6 @@ data class Contribution constructor( | ||||||
|         const val STATE_QUEUED = 2 |         const val STATE_QUEUED = 2 | ||||||
|         const val STATE_IN_PROGRESS = 3 |         const val STATE_IN_PROGRESS = 3 | ||||||
|         const val STATE_PAUSED = 4 |         const val STATE_PAUSED = 4 | ||||||
|         const val STATE_QUEUED_LIMITED_CONNECTION_MODE=5 |  | ||||||
| 
 | 
 | ||||||
|         /** |         /** | ||||||
|          * Formatting captions to the Wikibase format for sending labels |          * Formatting captions to the Wikibase format for sending labels | ||||||
|  | @ -127,11 +128,8 @@ data class Contribution constructor( | ||||||
|         return chunkInfo != null && chunkInfo!!.totalChunks == chunkInfo!!.indexOfNextChunkToUpload |         return chunkInfo != null && chunkInfo!!.totalChunks == chunkInfo!!.indexOfNextChunkToUpload | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     fun isPaused(): Boolean { |     fun dateUploadStartedInMillis(): Long { | ||||||
|         return CommonsApplication.pauseUploads[pageId] ?: false |         return dateUploadStarted!!.time | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     fun unpause() { |  | ||||||
|         CommonsApplication.pauseUploads[pageId] = false |  | ||||||
|     } |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -9,6 +9,10 @@ import android.content.Intent; | ||||||
| import android.widget.Toast; | import android.widget.Toast; | ||||||
| import androidx.activity.result.ActivityResultLauncher; | import androidx.activity.result.ActivityResultLauncher; | ||||||
| import androidx.annotation.NonNull; | 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.R; | ||||||
| import fr.free.nrw.commons.filepicker.DefaultCallback; | import fr.free.nrw.commons.filepicker.DefaultCallback; | ||||||
| import fr.free.nrw.commons.filepicker.FilePicker; | 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.PermissionUtils; | ||||||
| import fr.free.nrw.commons.utils.ViewUtil; | import fr.free.nrw.commons.utils.ViewUtil; | ||||||
| import java.util.ArrayList; | import java.util.ArrayList; | ||||||
|  | import java.util.Arrays; | ||||||
|  | import java.util.Collections; | ||||||
| import java.util.List; | import java.util.List; | ||||||
| import javax.inject.Inject; | import javax.inject.Inject; | ||||||
| import javax.inject.Named; | import javax.inject.Named; | ||||||
|  | @ -39,10 +45,16 @@ public class ContributionController { | ||||||
|     private boolean isInAppCameraUpload; |     private boolean isInAppCameraUpload; | ||||||
|     public LocationPermissionCallback locationPermissionCallback; |     public LocationPermissionCallback locationPermissionCallback; | ||||||
|     private LocationPermissionsHelper locationPermissionsHelper; |     private LocationPermissionsHelper locationPermissionsHelper; | ||||||
|  |     LiveData<PagedList<Contribution>> failedAndPendingContributionList; | ||||||
|  |     LiveData<PagedList<Contribution>> pendingContributionList; | ||||||
|  |     LiveData<PagedList<Contribution>> failedContributionList; | ||||||
| 
 | 
 | ||||||
|     @Inject |     @Inject | ||||||
|     LocationServiceManager locationManager; |     LocationServiceManager locationManager; | ||||||
| 
 | 
 | ||||||
|  |     @Inject | ||||||
|  |     ContributionsRepository repository; | ||||||
|  | 
 | ||||||
|     @Inject |     @Inject | ||||||
|     public ContributionController(@Named("default_preferences") JsonKvStore defaultKvStore) { |     public ContributionController(@Named("default_preferences") JsonKvStore defaultKvStore) { | ||||||
|         this.defaultKvStore = defaultKvStore; |         this.defaultKvStore = defaultKvStore; | ||||||
|  | @ -115,14 +127,14 @@ public class ContributionController { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Shows a dialog alerting the user about location services being off |      * Shows a dialog alerting the user about location services being off and asking them to turn it | ||||||
|      * and asking them to turn it on |      * on | ||||||
|      * TODO: Add a seperate callback in LocationPermissionsHelper for this. |      * TODO: Add a seperate callback in LocationPermissionsHelper for this. | ||||||
|      *      Ref: https://github.com/commons-app/apps-android-commons/pull/5494/files#r1510553114 |      *      Ref: https://github.com/commons-app/apps-android-commons/pull/5494/files#r1510553114 | ||||||
|      * |      * | ||||||
|      * @param activity Activity reference |      * @param activity           Activity reference | ||||||
|      * @param dialogTextResource Resource id of text to be shown in dialog |      * @param dialogTextResource Resource id of text to be shown in dialog | ||||||
|      * @param toastTextResource Resource id of text to be shown in toast |      * @param toastTextResource  Resource id of text to be shown in toast | ||||||
|      */ |      */ | ||||||
|     private void showLocationOffDialog(Activity activity, int dialogTextResource, |     private void showLocationOffDialog(Activity activity, int dialogTextResource, | ||||||
|         int toastTextResource) { |         int toastTextResource) { | ||||||
|  | @ -307,4 +319,60 @@ public class ContributionController { | ||||||
|         isInAppCameraUpload = false;    // reset the flag for next use |         isInAppCameraUpload = false;    // reset the flag for next use | ||||||
|         return shareIntent; |         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 io.reactivex.Single; | ||||||
| import java.util.Calendar; | import java.util.Calendar; | ||||||
| import java.util.List; | import java.util.List; | ||||||
|  | import timber.log.Timber; | ||||||
| 
 | 
 | ||||||
| @Dao | @Dao | ||||||
| public abstract class ContributionDao { | public abstract class ContributionDao { | ||||||
|  | @ -27,6 +28,9 @@ public abstract class ContributionDao { | ||||||
|         return Completable |         return Completable | ||||||
|             .fromAction(() -> { |             .fromAction(() -> { | ||||||
|                 contribution.setDateModified(Calendar.getInstance().getTime()); |                 contribution.setDateModified(Calendar.getInstance().getTime()); | ||||||
|  |                 if (contribution.getDateUploadStarted() == null) { | ||||||
|  |                     contribution.setDateUploadStarted(Calendar.getInstance().getTime()); | ||||||
|  |                 } | ||||||
|                 saveSynchronous(contribution); |                 saveSynchronous(contribution); | ||||||
|             }); |             }); | ||||||
|     } |     } | ||||||
|  | @ -44,11 +48,32 @@ public abstract class ContributionDao { | ||||||
|     @Delete |     @Delete | ||||||
|     public abstract void deleteSynchronous(Contribution contribution); |     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) { |     public Completable delete(final Contribution contribution) { | ||||||
|         return Completable |         return Completable | ||||||
|             .fromAction(() -> deleteSynchronous(contribution)); |             .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") |     @Query("SELECT * from contribution WHERE media_filename=:fileName") | ||||||
|     public abstract List<Contribution> getContributionWithTitle(String 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") |     @Query("SELECT * from contribution WHERE state IN (:states) order by media_dateUploaded DESC") | ||||||
|     public abstract Single<List<Contribution>> getContribution(List<Integer> states); |     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)") |     @Query("SELECT COUNT(*) from contribution WHERE state in (:toUpdateStates)") | ||||||
|     public abstract Single<Integer> getPendingUploads(int[] toUpdateStates); |     public abstract Single<Integer> getPendingUploads(int[] toUpdateStates); | ||||||
| 
 | 
 | ||||||
|  | @ -67,6 +112,15 @@ public abstract class ContributionDao { | ||||||
|     @Update |     @Update | ||||||
|     public abstract void updateSynchronous(Contribution contribution); |     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) { |     public Completable update(final Contribution contribution) { | ||||||
|         return Completable |         return Completable | ||||||
|             .fromAction(() -> { |             .fromAction(() -> { | ||||||
|  | @ -74,4 +128,18 @@ public abstract class ContributionDao { | ||||||
|                 updateSynchronous(contribution); |                 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 = LayoutContributionBinding.bind(parent); | ||||||
| 
 | 
 | ||||||
|         binding.retryButton.setOnClickListener(v -> retryUpload()); |  | ||||||
|         binding.cancelButton.setOnClickListener(v -> deleteUpload()); |  | ||||||
|         binding.contributionImage.setOnClickListener(v -> imageClicked()); |         binding.contributionImage.setOnClickListener(v -> imageClicked()); | ||||||
|         binding.wikipediaButton.setOnClickListener(v -> wikipediaButtonClicked()); |         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 |         /* Set a dialog indicating that the upload is being paused. This is needed because pausing | ||||||
|         an upload might take a dozen seconds. */ |         an upload might take a dozen seconds. */ | ||||||
|  | @ -79,9 +76,6 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder { | ||||||
| 
 | 
 | ||||||
|         binding.contributionImage.getHierarchy().setPlaceholderImage(R.drawable.image_placeholder); |         binding.contributionImage.getHierarchy().setPlaceholderImage(R.drawable.image_placeholder); | ||||||
|         binding.contributionImage.getHierarchy().setFailureImage(R.drawable.image_placeholder); |         binding.contributionImage.getHierarchy().setFailureImage(R.drawable.image_placeholder); | ||||||
|          |  | ||||||
|          |  | ||||||
|          |  | ||||||
| 
 | 
 | ||||||
|         final String imageSource = chooseImageSource(contribution.getMedia().getThumbUrl(), |         final String imageSource = chooseImageSource(contribution.getMedia().getThumbUrl(), | ||||||
|             contribution.getLocalUri()); |             contribution.getLocalUri()); | ||||||
|  | @ -90,79 +84,27 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder { | ||||||
|                 imageRequest = ImageRequestBuilder.newBuilderWithSource(Uri.parse(imageSource)) |                 imageRequest = ImageRequestBuilder.newBuilderWithSource(Uri.parse(imageSource)) | ||||||
|                     .setProgressiveRenderingEnabled(true) |                     .setProgressiveRenderingEnabled(true) | ||||||
|                     .build(); |                     .build(); | ||||||
|             } |             } else if (URLUtil.isFileUrl(imageSource)) { | ||||||
|             else if (URLUtil.isFileUrl(imageSource)){ |                 imageRequest = ImageRequest.fromUri(Uri.parse(imageSource)); | ||||||
|                 imageRequest=ImageRequest.fromUri(Uri.parse(imageSource)); |             } else if (imageSource != null) { | ||||||
|             } |  | ||||||
|             else if(imageSource != null) { |  | ||||||
|                 final File file = new File(imageSource); |                 final File file = new File(imageSource); | ||||||
|                 imageRequest = ImageRequest.fromFile(file); |                 imageRequest = ImageRequest.fromFile(file); | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             if(imageRequest != null){ |             if (imageRequest != null) { | ||||||
|                 binding.contributionImage.setImageRequest(imageRequest); |                 binding.contributionImage.setImageRequest(imageRequest); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         binding.contributionSequenceNumber.setText(String.valueOf(position + 1)); |         binding.contributionSequenceNumber.setText(String.valueOf(position + 1)); | ||||||
|         binding.contributionSequenceNumber.setVisibility(View.VISIBLE); |         binding.contributionSequenceNumber.setVisibility(View.VISIBLE); | ||||||
| 
 |  | ||||||
|         binding.wikipediaButton.setVisibility(View.GONE); |         binding.wikipediaButton.setVisibility(View.GONE); | ||||||
|         switch (contribution.getState()) { |         binding.contributionState.setVisibility(View.GONE); | ||||||
|             case Contribution.STATE_COMPLETED: |         binding.contributionProgress.setVisibility(View.GONE); | ||||||
|                 binding.contributionState.setVisibility(View.GONE); |         binding.imageOptions.setVisibility(View.GONE); | ||||||
|                 binding.contributionProgress.setVisibility(View.GONE); |         binding.contributionState.setText(""); | ||||||
|                 binding.imageOptions.setVisibility(View.GONE); |         checkIfMediaExistsOnWikipediaPage(contribution); | ||||||
|                 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) { |         if (!mediaExists) { | ||||||
|             binding.wikipediaButton.setVisibility(View.VISIBLE); |             binding.wikipediaButton.setVisibility(View.VISIBLE); | ||||||
|             isWikipediaButtonDisplayed = true; |             isWikipediaButtonDisplayed = true; | ||||||
|             binding.cancelButton.setVisibility(View.GONE); |  | ||||||
|             binding.retryButton.setVisibility(View.GONE); |  | ||||||
|             binding.imageOptions.setVisibility(View.VISIBLE); |             binding.imageOptions.setVisibility(View.VISIBLE); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  | @ -217,20 +157,6 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder { | ||||||
|                 null; |                 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() { |     public void imageClicked() { | ||||||
|         callback.openMediaDetail(position, isWikipediaButtonDisplayed); |         callback.openMediaDetail(position, isWikipediaButtonDisplayed); | ||||||
|     } |     } | ||||||
|  | @ -239,44 +165,6 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder { | ||||||
|         callback.addImageToWikipedia(contribution); |         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() { |     public ImageRequest getImageRequest() { | ||||||
|         return imageRequest; |         return imageRequest; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -19,8 +19,5 @@ public class ContributionsContract { | ||||||
| 
 | 
 | ||||||
|         Contribution getContributionsWithTitle(String uri); |         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.contributions.Contribution.STATE_PAUSED; | ||||||
| import static fr.free.nrw.commons.nearby.fragments.NearbyParentFragment.WLM_URL; | 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.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.computeBearing; | ||||||
| import static fr.free.nrw.commons.utils.LengthUtils.formatDistanceBetween; | import static fr.free.nrw.commons.utils.LengthUtils.formatDistanceBetween; | ||||||
| 
 | 
 | ||||||
|  | @ -12,6 +13,7 @@ import android.Manifest; | ||||||
| import android.Manifest.permission; | import android.Manifest.permission; | ||||||
| import android.annotation.SuppressLint; | import android.annotation.SuppressLint; | ||||||
| import android.content.Context; | import android.content.Context; | ||||||
|  | import android.content.Intent; | ||||||
| import android.hardware.Sensor; | import android.hardware.Sensor; | ||||||
| import android.hardware.SensorEvent; | import android.hardware.SensorEvent; | ||||||
| import android.hardware.SensorEventListener; | import android.hardware.SensorEventListener; | ||||||
|  | @ -25,6 +27,7 @@ import android.view.MenuItem.OnMenuItemClickListener; | ||||||
| import android.view.View; | import android.view.View; | ||||||
| import android.view.ViewGroup; | import android.view.ViewGroup; | ||||||
| import android.widget.CheckBox; | import android.widget.CheckBox; | ||||||
|  | import android.widget.ImageView; | ||||||
| import android.widget.LinearLayout; | import android.widget.LinearLayout; | ||||||
| import android.widget.TextView; | import android.widget.TextView; | ||||||
| import android.widget.Toast; | 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.notification.NotificationController; | ||||||
| import fr.free.nrw.commons.profile.ProfileActivity; | import fr.free.nrw.commons.profile.ProfileActivity; | ||||||
| import fr.free.nrw.commons.theme.BaseActivity; | 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.Date; | ||||||
| import java.util.List; | import java.util.List; | ||||||
| import java.util.Map; | import java.util.Map; | ||||||
|  | @ -104,6 +109,8 @@ public class ContributionsFragment | ||||||
|     LocationServiceManager locationManager; |     LocationServiceManager locationManager; | ||||||
|     @Inject |     @Inject | ||||||
|     NotificationController notificationController; |     NotificationController notificationController; | ||||||
|  |     @Inject | ||||||
|  |     ContributionController contributionController; | ||||||
| 
 | 
 | ||||||
|     private CompositeDisposable compositeDisposable = new CompositeDisposable(); |     private CompositeDisposable compositeDisposable = new CompositeDisposable(); | ||||||
| 
 | 
 | ||||||
|  | @ -113,10 +120,10 @@ public class ContributionsFragment | ||||||
|     static final String MEDIA_DETAIL_PAGER_FRAGMENT_TAG = "MediaDetailFragmentTag"; |     static final String MEDIA_DETAIL_PAGER_FRAGMENT_TAG = "MediaDetailFragmentTag"; | ||||||
|     private static final int MAX_RETRIES = 10; |     private static final int MAX_RETRIES = 10; | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|     public FragmentContributionsBinding binding; |     public FragmentContributionsBinding binding; | ||||||
| 
 | 
 | ||||||
|     @Inject ContributionsPresenter contributionsPresenter; |     @Inject | ||||||
|  |     ContributionsPresenter contributionsPresenter; | ||||||
| 
 | 
 | ||||||
|     @Inject |     @Inject | ||||||
|     SessionManager sessionManager; |     SessionManager sessionManager; | ||||||
|  | @ -129,6 +136,12 @@ public class ContributionsFragment | ||||||
| 
 | 
 | ||||||
|     public TextView notificationCount; |     public TextView notificationCount; | ||||||
| 
 | 
 | ||||||
|  |     public TextView pendingUploadsCountTextView; | ||||||
|  | 
 | ||||||
|  |     public TextView uploadsErrorTextView; | ||||||
|  | 
 | ||||||
|  |     public ImageView pendingUploadsImageView; | ||||||
|  | 
 | ||||||
|     private Campaign wlmCampaign; |     private Campaign wlmCampaign; | ||||||
| 
 | 
 | ||||||
|     String userName; |     String userName; | ||||||
|  | @ -147,20 +160,22 @@ public class ContributionsFragment | ||||||
|                     areAllGranted = areAllGranted && b; |                     areAllGranted = areAllGranted && b; | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|             if (areAllGranted) { |                 if (areAllGranted) { | ||||||
|                 onLocationPermissionGranted(); |                     onLocationPermissionGranted(); | ||||||
|             } else { |  | ||||||
|                 if (shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_FINE_LOCATION) |  | ||||||
|                     && store.getBoolean("displayLocationPermissionForCardView", true) |  | ||||||
|                     && !store.getBoolean("doNotAskForLocationPermission", false) |  | ||||||
|                     && (((MainActivity) getActivity()).activeFragment == ActiveFragment.CONTRIBUTIONS)) { |  | ||||||
|                     binding.cardViewNearby.permissionType = NearbyNotificationCardView.PermissionType.ENABLE_LOCATION_PERMISSION; |  | ||||||
|                 } else { |                 } else { | ||||||
|                     displayYouWontSeeNearbyMessage(); |                     if (shouldShowRequestPermissionRationale( | ||||||
|  |                         Manifest.permission.ACCESS_FINE_LOCATION) | ||||||
|  |                         && store.getBoolean("displayLocationPermissionForCardView", true) | ||||||
|  |                         && !store.getBoolean("doNotAskForLocationPermission", false) | ||||||
|  |                         && (((MainActivity) getActivity()).activeFragment | ||||||
|  |                         == ActiveFragment.CONTRIBUTIONS)) { | ||||||
|  |                         binding.cardViewNearby.permissionType = NearbyNotificationCardView.PermissionType.ENABLE_LOCATION_PERMISSION; | ||||||
|  |                     } else { | ||||||
|  |                         displayYouWontSeeNearbyMessage(); | ||||||
|  |                     } | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         } |         }); | ||||||
|     }); |  | ||||||
| 
 | 
 | ||||||
|     @NonNull |     @NonNull | ||||||
|     public static ContributionsFragment newInstance() { |     public static ContributionsFragment newInstance() { | ||||||
|  | @ -198,11 +213,10 @@ public class ContributionsFragment | ||||||
|         checkBox.setOnCheckedChangeListener((buttonView, isChecked) -> { |         checkBox.setOnCheckedChangeListener((buttonView, isChecked) -> { | ||||||
|             if (isChecked) { |             if (isChecked) { | ||||||
|                 // Do not ask for permission on activity start again |                 // Do not ask for permission on activity start again | ||||||
|                 store.putBoolean("displayLocationPermissionForCardView",false); |                 store.putBoolean("displayLocationPermissionForCardView", false); | ||||||
|             } |             } | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|         if (savedInstanceState != null) { |         if (savedInstanceState != null) { | ||||||
|             mediaDetailPagerFragment = (MediaDetailPagerFragment) getChildFragmentManager() |             mediaDetailPagerFragment = (MediaDetailPagerFragment) getChildFragmentManager() | ||||||
|                 .findFragmentByTag(MEDIA_DETAIL_PAGER_FRAGMENT_TAG); |                 .findFragmentByTag(MEDIA_DETAIL_PAGER_FRAGMENT_TAG); | ||||||
|  | @ -212,9 +226,7 @@ public class ContributionsFragment | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         initFragments(); |         initFragments(); | ||||||
|         if(isUserProfile) { |         if (!isUserProfile) { | ||||||
|             binding.limitedConnectionEnabledLayout.setVisibility(View.GONE); |  | ||||||
|         }else { |  | ||||||
|             upDateUploadCount(); |             upDateUploadCount(); | ||||||
|         } |         } | ||||||
|         if (shouldShowMediaDetailsFragment) { |         if (shouldShowMediaDetailsFragment) { | ||||||
|  | @ -230,7 +242,6 @@ public class ContributionsFragment | ||||||
|             && sessionManager.getCurrentAccount() != null && !isUserProfile) { |             && sessionManager.getCurrentAccount() != null && !isUserProfile) { | ||||||
|             setUploadCount(); |             setUploadCount(); | ||||||
|         } |         } | ||||||
|         binding.limitedConnectionEnabledLayout.setOnClickListener(toggleDescriptionListener); |  | ||||||
|         setHasOptionsMenu(true); |         setHasOptionsMenu(true); | ||||||
|         return binding.getRoot(); |         return binding.getRoot(); | ||||||
|     } |     } | ||||||
|  | @ -258,10 +269,32 @@ public class ContributionsFragment | ||||||
|         MenuItem notificationsMenuItem = menu.findItem(R.id.notifications); |         MenuItem notificationsMenuItem = menu.findItem(R.id.notifications); | ||||||
|         final View notification = notificationsMenuItem.getActionView(); |         final View notification = notificationsMenuItem.getActionView(); | ||||||
|         notificationCount = notification.findViewById(R.id.notification_count_badge); |         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 -> { |         notification.setOnClickListener(view -> { | ||||||
|             NotificationActivity.startYourself(getContext(), "unread"); |             NotificationActivity.startYourself(getContext(), "unread"); | ||||||
|         }); |         }); | ||||||
|         updateLimitedConnectionToggle(menu); |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @SuppressLint("CheckResult") |     @SuppressLint("CheckResult") | ||||||
|  | @ -273,6 +306,33 @@ public class ContributionsFragment | ||||||
|                 throwable -> Timber.e(throwable, "Error occurred while loading notifications"))); |                 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() { |     public void scrollToTop() { | ||||||
|         if (contributionsListFragment != null) { |         if (contributionsListFragment != null) { | ||||||
|             contributionsListFragment.scrollToTop(); |             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 |     @Override | ||||||
|     public void onAttach(Context context) { |     public void onAttach(Context context) { | ||||||
|         super.onAttach(context); |         super.onAttach(context); | ||||||
|  | @ -355,7 +392,7 @@ public class ContributionsFragment | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private void setupViewForMediaDetails() { |     private void setupViewForMediaDetails() { | ||||||
|         if (binding!=null) { |         if (binding != null) { | ||||||
|             binding.campaignsView.setVisibility(View.GONE); |             binding.campaignsView.setVisibility(View.GONE); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  | @ -465,7 +502,7 @@ public class ContributionsFragment | ||||||
|         contributionsPresenter.onAttachView(this); |         contributionsPresenter.onAttachView(this); | ||||||
|         locationManager.addLocationListener(this); |         locationManager.addLocationListener(this); | ||||||
| 
 | 
 | ||||||
|         if (binding==null) { |         if (binding == null) { | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  | @ -484,7 +521,8 @@ public class ContributionsFragment | ||||||
|                 } catch (Exception e) { |                 } catch (Exception e) { | ||||||
|                     Timber.e(e); |                     Timber.e(e); | ||||||
|                 } |                 } | ||||||
|                 if (binding.cardViewNearby.cardViewVisibilityState == NearbyNotificationCardView.CardViewVisibilityState.READY) { |                 if (binding.cardViewNearby.cardViewVisibilityState | ||||||
|  |                     == NearbyNotificationCardView.CardViewVisibilityState.READY) { | ||||||
|                     binding.cardViewNearby.setVisibility(View.VISIBLE); |                     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 |             // Notification Count and Campaigns should not be set, if it is used in User Profile | ||||||
|             if(!isUserProfile) { |             if (!isUserProfile) { | ||||||
|                 setNotificationCount(); |                 setNotificationCount(); | ||||||
|                 fetchCampaigns(); |                 fetchCampaigns(); | ||||||
|  |                 setUploadIconVisibility(); | ||||||
|  |                 setUploadIconCount(); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         mSensorManager.registerListener(this, mLight, SensorManager.SENSOR_DELAY_UI); |         mSensorManager.registerListener(this, mLight, SensorManager.SENSOR_DELAY_UI); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private void checkPermissionsAndShowNearbyCardView() { |     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(); |             onLocationPermissionGranted(); | ||||||
|         } else if (shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_FINE_LOCATION) |         } else if (shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_FINE_LOCATION) | ||||||
|             && store.getBoolean("displayLocationPermissionForCardView", true) |             && store.getBoolean("displayLocationPermissionForCardView", true) | ||||||
|  | @ -636,14 +677,14 @@ public class ContributionsFragment | ||||||
|      */ |      */ | ||||||
|     private void fetchCampaigns() { |     private void fetchCampaigns() { | ||||||
|         if (Utils.isMonumentsEnabled(new Date())) { |         if (Utils.isMonumentsEnabled(new Date())) { | ||||||
|             if (binding!=null) { |             if (binding != null) { | ||||||
|                 binding.campaignsView.setCampaign(wlmCampaign); |                 binding.campaignsView.setCampaign(wlmCampaign); | ||||||
|                 binding.campaignsView.setVisibility(View.VISIBLE); |                 binding.campaignsView.setVisibility(View.VISIBLE); | ||||||
|             } |             } | ||||||
|         } else if (store.getBoolean(CampaignView.CAMPAIGNS_DEFAULT_PREFERENCE, true)) { |         } else if (store.getBoolean(CampaignView.CAMPAIGNS_DEFAULT_PREFERENCE, true)) { | ||||||
|             presenter.getCampaigns(); |             presenter.getCampaigns(); | ||||||
|         } else { |         } else { | ||||||
|             if (binding!=null) { |             if (binding != null) { | ||||||
|                 binding.campaignsView.setVisibility(View.GONE); |                 binding.campaignsView.setVisibility(View.GONE); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  | @ -657,7 +698,7 @@ public class ContributionsFragment | ||||||
|     @Override |     @Override | ||||||
|     public void showCampaigns(Campaign campaign) { |     public void showCampaigns(Campaign campaign) { | ||||||
|         if (campaign != null && !isUserProfile) { |         if (campaign != null && !isUserProfile) { | ||||||
|             if (binding!=null) { |             if (binding != null) { | ||||||
|                 binding.campaignsView.setCampaign(campaign); |                 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. |      * 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 |      * Replace whatever is in the current contributionsFragmentContainer view with | ||||||
|      * mediaDetailPagerFragment, and preserve previous state in back stack. Called when user selects |      * mediaDetailPagerFragment, and preserve previous state in back stack. Called when user selects | ||||||
|  | @ -782,7 +810,8 @@ public class ContributionsFragment | ||||||
|     public boolean backButtonClicked() { |     public boolean backButtonClicked() { | ||||||
|         if (mediaDetailPagerFragment != null && mediaDetailPagerFragment.isVisible()) { |         if (mediaDetailPagerFragment != null && mediaDetailPagerFragment.isVisible()) { | ||||||
|             if (store.getBoolean("displayNearbyCardView", true) && !isUserProfile) { |             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); |                     binding.cardViewNearby.setVisibility(View.VISIBLE); | ||||||
|                 } |                 } | ||||||
|             } else { |             } 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 |      * 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. |      * When the device rotates, rotate the Nearby banner's compass arrow in tandem. | ||||||
|      */ |      */ | ||||||
|  |  | ||||||
|  | @ -70,16 +70,8 @@ public class ContributionsListAdapter extends | ||||||
| 
 | 
 | ||||||
|     public interface Callback { |     public interface Callback { | ||||||
| 
 | 
 | ||||||
|         void retryUpload(Contribution contribution); |  | ||||||
| 
 |  | ||||||
|         void deleteUpload(Contribution contribution); |  | ||||||
| 
 |  | ||||||
|         void openMediaDetail(int contribution, boolean isWikipediaPageExists); |         void openMediaDetail(int contribution, boolean isWikipediaPageExists); | ||||||
| 
 | 
 | ||||||
|         void addImageToWikipedia(Contribution contribution); |         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> { |     public interface UserActionListener extends BasePresenter<View> { | ||||||
| 
 |  | ||||||
|         void deleteUpload(Contribution contribution); |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -19,7 +19,7 @@ import android.view.animation.AnimationUtils; | ||||||
| import android.widget.LinearLayout; | import android.widget.LinearLayout; | ||||||
| import androidx.activity.result.ActivityResultCallback; | import androidx.activity.result.ActivityResultCallback; | ||||||
| import androidx.activity.result.ActivityResultLauncher; | 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.NonNull; | ||||||
| import androidx.annotation.Nullable; | import androidx.annotation.Nullable; | ||||||
| import androidx.annotation.VisibleForTesting; | 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.ItemAnimator; | ||||||
| import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener; | import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener; | ||||||
| import androidx.recyclerview.widget.SimpleItemAnimator; | import androidx.recyclerview.widget.SimpleItemAnimator; | ||||||
| import fr.free.nrw.commons.CommonsApplication; |  | ||||||
| import fr.free.nrw.commons.Media; | import fr.free.nrw.commons.Media; | ||||||
| import fr.free.nrw.commons.R; | import fr.free.nrw.commons.R; | ||||||
| import fr.free.nrw.commons.Utils; | import fr.free.nrw.commons.Utils; | ||||||
| import fr.free.nrw.commons.auth.SessionManager; | 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.databinding.FragmentContributionsListBinding; | ||||||
| import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; | import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; | ||||||
| import fr.free.nrw.commons.media.MediaClient; | 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.DialogUtil; | ||||||
| import fr.free.nrw.commons.utils.SystemThemeUtils; | import fr.free.nrw.commons.utils.SystemThemeUtils; | ||||||
| import fr.free.nrw.commons.utils.ViewUtil; | import fr.free.nrw.commons.utils.ViewUtil; | ||||||
| import java.util.Locale; |  | ||||||
| import java.util.Map; | import java.util.Map; | ||||||
| import java.util.Objects; | import java.util.Objects; | ||||||
| import javax.inject.Inject; | import javax.inject.Inject; | ||||||
|  | @ -56,7 +55,7 @@ import fr.free.nrw.commons.wikidata.model.WikiSite; | ||||||
|  */ |  */ | ||||||
| 
 | 
 | ||||||
| public class ContributionsListFragment extends CommonsDaggerSupportFragment implements | public class ContributionsListFragment extends CommonsDaggerSupportFragment implements | ||||||
|     ContributionsListContract.View, ContributionsListAdapter.Callback, |     ContributionsListContract.View, Callback, | ||||||
|     WikipediaInstructionsDialogFragment.Callback { |     WikipediaInstructionsDialogFragment.Callback { | ||||||
| 
 | 
 | ||||||
|     private static final String RV_STATE = "rv_scroll_state"; |     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_forward; | ||||||
|     private Animation rotate_backward; |     private Animation rotate_backward; | ||||||
|     private boolean isFabOpen; |     private boolean isFabOpen; | ||||||
| 
 |  | ||||||
|     @VisibleForTesting |     @VisibleForTesting | ||||||
|     protected RecyclerView rvContributionsList; |     protected RecyclerView rvContributionsList; | ||||||
| 
 | 
 | ||||||
|  | @ -99,7 +97,7 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl | ||||||
|     private String userName; |     private String userName; | ||||||
| 
 | 
 | ||||||
|     private ActivityResultLauncher<String[]> inAppCameraLocationPermissionLauncher = registerForActivityResult( |     private ActivityResultLauncher<String[]> inAppCameraLocationPermissionLauncher = registerForActivityResult( | ||||||
|         new ActivityResultContracts.RequestMultiplePermissions(), |         new RequestMultiplePermissions(), | ||||||
|         new ActivityResultCallback<Map<String, Boolean>>() { |         new ActivityResultCallback<Map<String, Boolean>>() { | ||||||
|             @Override |             @Override | ||||||
|             public void onActivityResult(Map<String, Boolean> result) { |             public void onActivityResult(Map<String, Boolean> result) { | ||||||
|  | @ -151,7 +149,7 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl | ||||||
|         contributionsListPresenter.onAttachView(this); |         contributionsListPresenter.onAttachView(this); | ||||||
|         binding.fabCustomGallery.setOnClickListener(v -> launchCustomSelector()); |         binding.fabCustomGallery.setOnClickListener(v -> launchCustomSelector()); | ||||||
|         binding.fabCustomGallery.setOnLongClickListener(view -> { |         binding.fabCustomGallery.setOnLongClickListener(view -> { | ||||||
|             ViewUtil.showShortToast(getContext(),R.string.custom_selector_title); |             ViewUtil.showShortToast(getContext(), R.string.custom_selector_title); | ||||||
|             return true; |             return true; | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|  | @ -160,7 +158,8 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl | ||||||
|             binding.fabLayout.setVisibility(VISIBLE); |             binding.fabLayout.setVisibility(VISIBLE); | ||||||
|         } else { |         } else { | ||||||
|             binding.tvContributionsOfUser.setVisibility(VISIBLE); |             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); |             binding.fabLayout.setVisibility(GONE); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  | @ -305,8 +304,9 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl | ||||||
|     public void onConfigurationChanged(final Configuration newConfig) { |     public void onConfigurationChanged(final Configuration newConfig) { | ||||||
|         super.onConfigurationChanged(newConfig); |         super.onConfigurationChanged(newConfig); | ||||||
|         // check orientation |         // check orientation | ||||||
|         binding.fabLayout.setOrientation(newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE ? |         binding.fabLayout.setOrientation( | ||||||
|             LinearLayout.HORIZONTAL : LinearLayout.VERTICAL); |             newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE ? | ||||||
|  |                 LinearLayout.HORIZONTAL : LinearLayout.VERTICAL); | ||||||
|         rvContributionsList |         rvContributionsList | ||||||
|             .setLayoutManager( |             .setLayoutManager( | ||||||
|                 new GridLayoutManager(getContext(), getSpanCount(newConfig.orientation))); |                 new GridLayoutManager(getContext(), getSpanCount(newConfig.orientation))); | ||||||
|  | @ -326,7 +326,7 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl | ||||||
|             animateFAB(isFabOpen); |             animateFAB(isFabOpen); | ||||||
|         }); |         }); | ||||||
|         binding.fabCamera.setOnLongClickListener(view -> { |         binding.fabCamera.setOnLongClickListener(view -> { | ||||||
|             ViewUtil.showShortToast(getContext(),R.string.add_contribution_from_camera); |             ViewUtil.showShortToast(getContext(), R.string.add_contribution_from_camera); | ||||||
|             return true; |             return true; | ||||||
|         }); |         }); | ||||||
|         binding.fabGallery.setOnClickListener(view -> { |         binding.fabGallery.setOnClickListener(view -> { | ||||||
|  | @ -334,7 +334,7 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl | ||||||
|             animateFAB(isFabOpen); |             animateFAB(isFabOpen); | ||||||
|         }); |         }); | ||||||
|         binding.fabGallery.setOnLongClickListener(view -> { |         binding.fabGallery.setOnLongClickListener(view -> { | ||||||
|             ViewUtil.showShortToast(getContext(),R.string.menu_from_gallery); |             ViewUtil.showShortToast(getContext(), R.string.menu_from_gallery); | ||||||
|             return true; |             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 |     @Override | ||||||
|     public void openMediaDetail(final int position, boolean isWikipediaButtonDisplayed) { |     public void openMediaDetail(final int position, boolean isWikipediaButtonDisplayed) { | ||||||
|         if (null != callback) {//Just being safe, ideally they won't be called when detached |         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 |      * 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 notifyDataSetChanged(); | ||||||
| 
 | 
 | ||||||
|         void retryUpload(Contribution contribution); |  | ||||||
| 
 |  | ||||||
|         void showDetail(int position, boolean isWikipediaButtonDisplayed); |         void showDetail(int position, boolean isWikipediaButtonDisplayed); | ||||||
| 
 | 
 | ||||||
|         void pauseUpload(Contribution contribution); |  | ||||||
| 
 |  | ||||||
|         // Notify the viewpager that number of items have changed. |         // Notify the viewpager that number of items have changed. | ||||||
|         void viewPagerNotifyDataSetChanged(); |         void viewPagerNotifyDataSetChanged(); | ||||||
|  | 
 | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -10,6 +10,8 @@ import fr.free.nrw.commons.contributions.ContributionsListContract.UserActionLis | ||||||
| import fr.free.nrw.commons.di.CommonsApplicationModule; | import fr.free.nrw.commons.di.CommonsApplicationModule; | ||||||
| import io.reactivex.Scheduler; | import io.reactivex.Scheduler; | ||||||
| import io.reactivex.disposables.CompositeDisposable; | import io.reactivex.disposables.CompositeDisposable; | ||||||
|  | import java.util.Arrays; | ||||||
|  | import java.util.Collections; | ||||||
| import javax.inject.Inject; | import javax.inject.Inject; | ||||||
| import javax.inject.Named; | import javax.inject.Named; | ||||||
| 
 | 
 | ||||||
|  | @ -36,7 +38,7 @@ public class ContributionsListPresenter implements UserActionListener { | ||||||
|         this.contributionBoundaryCallback = contributionBoundaryCallback; |         this.contributionBoundaryCallback = contributionBoundaryCallback; | ||||||
|         this.repository = repository; |         this.repository = repository; | ||||||
|         this.ioThreadScheduler = ioThreadScheduler; |         this.ioThreadScheduler = ioThreadScheduler; | ||||||
|         this.contributionsRemoteDataSource=contributionsRemoteDataSource; |         this.contributionsRemoteDataSource = contributionsRemoteDataSource; | ||||||
|         compositeDisposable = new CompositeDisposable(); |         compositeDisposable = new CompositeDisposable(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -71,10 +73,12 @@ public class ContributionsListPresenter implements UserActionListener { | ||||||
|         } else { |         } else { | ||||||
|             contributionBoundaryCallback.setUserName(userName); |             contributionBoundaryCallback.setUserName(userName); | ||||||
|             shouldSetBoundaryCallback = true; |             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) { |         if (shouldSetBoundaryCallback) { | ||||||
|             livePagedListBuilder.setBoundaryCallback(contributionBoundaryCallback); |             livePagedListBuilder.setBoundaryCallback(contributionBoundaryCallback); | ||||||
|         } |         } | ||||||
|  | @ -89,15 +93,4 @@ public class ContributionsListPresenter implements UserActionListener { | ||||||
|         contributionBoundaryCallback.dispose(); |         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; | package fr.free.nrw.commons.contributions; | ||||||
| 
 | 
 | ||||||
| import androidx.paging.DataSource.Factory; | import androidx.paging.DataSource.Factory; | ||||||
|  | import fr.free.nrw.commons.kvstore.JsonKvStore; | ||||||
| import io.reactivex.Completable; | import io.reactivex.Completable; | ||||||
|  | import io.reactivex.Single; | ||||||
| import java.util.ArrayList; | import java.util.ArrayList; | ||||||
| import java.util.List; | import java.util.List; | ||||||
| 
 |  | ||||||
| import javax.inject.Inject; | import javax.inject.Inject; | ||||||
| import javax.inject.Named; | import javax.inject.Named; | ||||||
| 
 | 
 | ||||||
| import fr.free.nrw.commons.kvstore.JsonKvStore; |  | ||||||
| import io.reactivex.Single; |  | ||||||
| 
 |  | ||||||
| /** | /** | ||||||
|  * The LocalDataSource class for Contributions |  * The LocalDataSource class for Contributions | ||||||
|  */ |  */ | ||||||
|  | @ -21,8 +19,8 @@ class ContributionsLocalDataSource { | ||||||
| 
 | 
 | ||||||
|     @Inject |     @Inject | ||||||
|     public ContributionsLocalDataSource( |     public ContributionsLocalDataSource( | ||||||
|             @Named("default_preferences") final JsonKvStore defaultKVStore, |         @Named("default_preferences") final JsonKvStore defaultKVStore, | ||||||
|             final ContributionDao contributionDao) { |         final ContributionDao contributionDao) { | ||||||
|         this.defaultKVStore = defaultKVStore; |         this.defaultKVStore = defaultKVStore; | ||||||
|         this.contributionDao = contributionDao; |         this.contributionDao = contributionDao; | ||||||
|     } |     } | ||||||
|  | @ -38,17 +36,19 @@ class ContributionsLocalDataSource { | ||||||
|      * Fetch default number of contributions to be show, based on user preferences |      * Fetch default number of contributions to be show, based on user preferences | ||||||
|      */ |      */ | ||||||
|     public long getLong(final String key) { |     public long getLong(final String key) { | ||||||
|        return defaultKVStore.getLong(key); |         return defaultKVStore.getLong(key); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Get contribution object from cursor |      * Get contribution object from cursor | ||||||
|  |      * | ||||||
|      * @param uri |      * @param uri | ||||||
|      * @return |      * @return | ||||||
|      */ |      */ | ||||||
|     public Contribution getContributionWithFileName(final String uri) { |     public Contribution getContributionWithFileName(final String uri) { | ||||||
|         final List<Contribution> contributionWithUri = contributionDao.getContributionWithTitle(uri); |         final List<Contribution> contributionWithUri = contributionDao.getContributionWithTitle( | ||||||
|         if(!contributionWithUri.isEmpty()){ |             uri); | ||||||
|  |         if (!contributionWithUri.isEmpty()) { | ||||||
|             return contributionWithUri.get(0); |             return contributionWithUri.get(0); | ||||||
|         } |         } | ||||||
|         return null; |         return null; | ||||||
|  | @ -56,6 +56,7 @@ class ContributionsLocalDataSource { | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Remove a contribution from the contributions table |      * Remove a contribution from the contributions table | ||||||
|  |      * | ||||||
|      * @param contribution |      * @param contribution | ||||||
|      * @return |      * @return | ||||||
|      */ |      */ | ||||||
|  | @ -63,15 +64,48 @@ class ContributionsLocalDataSource { | ||||||
|         return contributionDao.delete(contribution); |         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() { |     public Factory<Integer, Contribution> getContributions() { | ||||||
|         return contributionDao.fetchContributions(); |         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) { |     public Single<List<Long>> saveContributions(final List<Contribution> contributions) { | ||||||
|         final List<Contribution> contributionList = new ArrayList<>(); |         final List<Contribution> contributionList = new ArrayList<>(); | ||||||
|         for(final Contribution contribution: contributions) { |         for (final Contribution contribution : contributions) { | ||||||
|             final Contribution oldContribution = contributionDao.getContribution(contribution.getPageId()); |             final Contribution oldContribution = contributionDao.getContribution( | ||||||
|             if(oldContribution != null) { |                 contribution.getPageId()); | ||||||
|  |             if (oldContribution != null) { | ||||||
|                 contribution.setWikidataPlace(oldContribution.getWikidataPlace()); |                 contribution.setWikidataPlace(oldContribution.getWikidataPlace()); | ||||||
|             } |             } | ||||||
|             contributionList.add(contribution); |             contributionList.add(contribution); | ||||||
|  | @ -84,10 +118,14 @@ class ContributionsLocalDataSource { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public void set(final String key, final long value) { |     public void set(final String key, final long value) { | ||||||
|         defaultKVStore.putLong(key,value); |         defaultKVStore.putLong(key, value); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public Completable updateContribution(final Contribution contribution) { |     public Completable updateContribution(final Contribution contribution) { | ||||||
|         return contributionDao.update(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; | package fr.free.nrw.commons.contributions; | ||||||
| 
 | 
 | ||||||
|  | import static fr.free.nrw.commons.utils.ImageUtils.IMAGE_OK; | ||||||
|  | 
 | ||||||
| import androidx.work.ExistingWorkPolicy; | import androidx.work.ExistingWorkPolicy; | ||||||
| import fr.free.nrw.commons.MediaDataExtractor; | import fr.free.nrw.commons.MediaDataExtractor; | ||||||
| import fr.free.nrw.commons.contributions.ContributionsContract.UserActionListener; | import fr.free.nrw.commons.contributions.ContributionsContract.UserActionListener; | ||||||
| import fr.free.nrw.commons.di.CommonsApplicationModule; | import fr.free.nrw.commons.di.CommonsApplicationModule; | ||||||
|  | import fr.free.nrw.commons.repository.UploadRepository; | ||||||
| import fr.free.nrw.commons.upload.worker.WorkRequestHelper; | import fr.free.nrw.commons.upload.worker.WorkRequestHelper; | ||||||
| import io.reactivex.Scheduler; | import io.reactivex.Scheduler; | ||||||
| import io.reactivex.disposables.CompositeDisposable; | import io.reactivex.disposables.CompositeDisposable; | ||||||
| import javax.inject.Inject; | import javax.inject.Inject; | ||||||
| import javax.inject.Named; | import javax.inject.Named; | ||||||
|  | import timber.log.Timber; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * The presenter class for Contributions |  * The presenter class for Contributions | ||||||
|  */ |  */ | ||||||
| public class ContributionsPresenter implements UserActionListener { | public class ContributionsPresenter implements UserActionListener { | ||||||
| 
 | 
 | ||||||
|     private final ContributionsRepository repository; |     private final ContributionsRepository contributionsRepository; | ||||||
|  |     private final UploadRepository uploadRepository; | ||||||
|     private final Scheduler ioThreadScheduler; |     private final Scheduler ioThreadScheduler; | ||||||
|     private CompositeDisposable compositeDisposable; |     private CompositeDisposable compositeDisposable; | ||||||
|     private ContributionsContract.View view; |     private ContributionsContract.View view; | ||||||
|  | @ -25,15 +30,17 @@ public class ContributionsPresenter implements UserActionListener { | ||||||
| 
 | 
 | ||||||
|     @Inject |     @Inject | ||||||
|     ContributionsPresenter(ContributionsRepository repository, |     ContributionsPresenter(ContributionsRepository repository, | ||||||
|  |         UploadRepository uploadRepository, | ||||||
|         @Named(CommonsApplicationModule.IO_THREAD) Scheduler ioThreadScheduler) { |         @Named(CommonsApplicationModule.IO_THREAD) Scheduler ioThreadScheduler) { | ||||||
|         this.repository = repository; |         this.contributionsRepository = repository; | ||||||
|         this.ioThreadScheduler=ioThreadScheduler; |         this.uploadRepository = uploadRepository; | ||||||
|  |         this.ioThreadScheduler = ioThreadScheduler; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|     public void onAttachView(ContributionsContract.View view) { |     public void onAttachView(ContributionsContract.View view) { | ||||||
|         this.view = view; |         this.view = view; | ||||||
|         compositeDisposable=new CompositeDisposable(); |         compositeDisposable = new CompositeDisposable(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|  | @ -44,19 +51,30 @@ public class ContributionsPresenter implements UserActionListener { | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|     public Contribution getContributionsWithTitle(String title) { |     public Contribution getContributionsWithTitle(String title) { | ||||||
|         return repository.getContributionWithFileName(title); |         return contributionsRepository.getContributionWithFileName(title); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Delete a failed contribution from the local db |      * Checks if a contribution is a duplicate and restarts the contribution process if it is not. | ||||||
|      * @param contribution |      * | ||||||
|  |      * @param contribution The contribution to check and potentially restart. | ||||||
|      */ |      */ | ||||||
|     @Override |     public void checkDuplicateImageAndRestartContribution(Contribution contribution) { | ||||||
|     public void deleteUpload(Contribution contribution) { |         compositeDisposable.add(uploadRepository | ||||||
|         compositeDisposable.add(repository |             .checkDuplicateImage(contribution.getLocalUriPath().getPath()) | ||||||
|             .deleteContributionFromDB(contribution) |  | ||||||
|             .subscribeOn(ioThreadScheduler) |             .subscribeOn(ioThreadScheduler) | ||||||
|             .subscribe()); |             .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()); | ||||||
|  |                 } | ||||||
|  |             })); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  | @ -65,9 +83,8 @@ public class ContributionsPresenter implements UserActionListener { | ||||||
|      * |      * | ||||||
|      * @param contribution |      * @param contribution | ||||||
|      */ |      */ | ||||||
|     @Override |  | ||||||
|     public void saveContribution(Contribution contribution) { |     public void saveContribution(Contribution contribution) { | ||||||
|         compositeDisposable.add(repository |         compositeDisposable.add(contributionsRepository | ||||||
|             .save(contribution) |             .save(contribution) | ||||||
|             .subscribeOn(ioThreadScheduler) |             .subscribeOn(ioThreadScheduler) | ||||||
|             .subscribe(() -> WorkRequestHelper.Companion.makeOneTimeWorkRequest( |             .subscribe(() -> WorkRequestHelper.Companion.makeOneTimeWorkRequest( | ||||||
|  |  | ||||||
|  | @ -29,6 +29,7 @@ public class ContributionsRepository { | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Deletes a failed upload from DB |      * Deletes a failed upload from DB | ||||||
|  |      * | ||||||
|      * @param contribution |      * @param contribution | ||||||
|      * @return |      * @return | ||||||
|      */ |      */ | ||||||
|  | @ -36,8 +37,19 @@ public class ContributionsRepository { | ||||||
|         return localDataSource.deleteContribution(contribution); |         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 |      * Get contribution object with title | ||||||
|  |      * | ||||||
|      * @param fileName |      * @param fileName | ||||||
|      * @return |      * @return | ||||||
|      */ |      */ | ||||||
|  | @ -49,19 +61,52 @@ public class ContributionsRepository { | ||||||
|         return localDataSource.getContributions(); |         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) { |     public Single<List<Long>> save(List<Contribution> contributions) { | ||||||
|         return localDataSource.saveContributions(contributions); |         return localDataSource.saveContributions(contributions); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public Completable save(Contribution contributions){ |     public Completable save(Contribution contributions) { | ||||||
|         return localDataSource.saveContributions(contributions); |         return localDataSource.saveContributions(contributions); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public void set(String key, long value) { |     public void set(String key, long value) { | ||||||
|         localDataSource.set(key,value); |         localDataSource.set(key, value); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public Completable updateContribution(Contribution contribution) { |     public Completable updateContribution(Contribution contribution) { | ||||||
|         return localDataSource.updateContribution(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,18 +41,21 @@ import fr.free.nrw.commons.notification.NotificationController; | ||||||
| import fr.free.nrw.commons.quiz.QuizChecker; | import fr.free.nrw.commons.quiz.QuizChecker; | ||||||
| import fr.free.nrw.commons.settings.SettingsFragment; | import fr.free.nrw.commons.settings.SettingsFragment; | ||||||
| import fr.free.nrw.commons.theme.BaseActivity; | 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.upload.worker.WorkRequestHelper; | ||||||
| import fr.free.nrw.commons.utils.PermissionUtils; | import fr.free.nrw.commons.utils.PermissionUtils; | ||||||
| import fr.free.nrw.commons.utils.ViewUtilWrapper; | import fr.free.nrw.commons.utils.ViewUtilWrapper; | ||||||
| import io.reactivex.Completable; | import io.reactivex.Completable; | ||||||
| import io.reactivex.schedulers.Schedulers; | import io.reactivex.schedulers.Schedulers; | ||||||
|  | import java.util.Calendar; | ||||||
| import java.util.Collections; | import java.util.Collections; | ||||||
| import java.util.List; | import java.util.List; | ||||||
| import javax.inject.Inject; | import javax.inject.Inject; | ||||||
| import javax.inject.Named; | import javax.inject.Named; | ||||||
| import timber.log.Timber; | import timber.log.Timber; | ||||||
| 
 | 
 | ||||||
| public class MainActivity  extends BaseActivity | public class MainActivity extends BaseActivity | ||||||
|     implements FragmentManager.OnBackStackChangedListener { |     implements FragmentManager.OnBackStackChangedListener { | ||||||
| 
 | 
 | ||||||
|     @Inject |     @Inject | ||||||
|  | @ -144,16 +147,16 @@ public class MainActivity  extends BaseActivity | ||||||
|                 applicationKvStore.putBoolean("hasAlreadyLaunchedBigMultiupload", false); |                 applicationKvStore.putBoolean("hasAlreadyLaunchedBigMultiupload", false); | ||||||
|                 applicationKvStore.putBoolean("hasAlreadyLaunchedCategoriesDialog", false); |                 applicationKvStore.putBoolean("hasAlreadyLaunchedCategoriesDialog", false); | ||||||
|             } |             } | ||||||
|             if(savedInstanceState == null){ |             if (savedInstanceState == null) { | ||||||
|                 //starting a fresh fragment. |                 //starting a fresh fragment. | ||||||
|                 // Open Last opened screen if it is Contributions or Nearby, otherwise Contributions |                 // 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)); |                     setTitle(getString(R.string.nearby_fragment)); | ||||||
|                     showNearby(); |                     showNearby(); | ||||||
|                     loadFragment(NearbyParentFragment.newInstance(),false); |                     loadFragment(NearbyParentFragment.newInstance(), false); | ||||||
|                 }else{ |                 } else { | ||||||
|                     setTitle(getString(R.string.contributions_fragment)); |                     setTitle(getString(R.string.contributions_fragment)); | ||||||
|                     loadFragment(ContributionsFragment.newInstance(),false); |                     loadFragment(ContributionsFragment.newInstance(), false); | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|             setUpPager(); |             setUpPager(); | ||||||
|  | @ -172,7 +175,6 @@ public class MainActivity  extends BaseActivity | ||||||
| //                    R.string.add_location_manually, | //                    R.string.add_location_manually, | ||||||
| //                    permission.ACCESS_MEDIA_LOCATION); | //                    permission.ACCESS_MEDIA_LOCATION); | ||||||
| //            } | //            } | ||||||
| 
 |  | ||||||
|             checkAndResumeStuckUploads(); |             checkAndResumeStuckUploads(); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  | @ -182,32 +184,33 @@ public class MainActivity  extends BaseActivity | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private void setUpPager() { |     private void setUpPager() { | ||||||
|         binding.fragmentMainNavTabLayout.setOnNavigationItemSelectedListener(navListener = (item) -> { |         binding.fragmentMainNavTabLayout.setOnNavigationItemSelectedListener( | ||||||
|             if (!item.getTitle().equals(getString(R.string.more))) { |             navListener = (item) -> { | ||||||
|                 // do not change title for more fragment |                 if (!item.getTitle().equals(getString(R.string.more))) { | ||||||
|                 setTitle(item.getTitle()); |                     // do not change title for more fragment | ||||||
|             } |                     setTitle(item.getTitle()); | ||||||
|             // set last_opened_nearby true if item is nearby screen else set false |                 } | ||||||
|             applicationKvStore.putBoolean("last_opened_nearby", |                 // set last_opened_nearby true if item is nearby screen else set false | ||||||
|                 item.getTitle().equals(getString(R.string.nearby_fragment))); |                 applicationKvStore.putBoolean("last_opened_nearby", | ||||||
|             final Fragment fragment = NavTab.of(item.getOrder()).newInstance(); |                     item.getTitle().equals(getString(R.string.nearby_fragment))); | ||||||
|             return loadFragment(fragment, true); |                 final Fragment fragment = NavTab.of(item.getOrder()).newInstance(); | ||||||
|         }); |                 return loadFragment(fragment, true); | ||||||
|  |             }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private void setUpLoggedOutPager() { |     private void setUpLoggedOutPager() { | ||||||
|         loadFragment(ExploreFragment.newInstance(),false); |         loadFragment(ExploreFragment.newInstance(), false); | ||||||
|         binding.fragmentMainNavTabLayout.setOnNavigationItemSelectedListener(item -> { |         binding.fragmentMainNavTabLayout.setOnNavigationItemSelectedListener(item -> { | ||||||
|             if (!item.getTitle().equals(getString(R.string.more))) { |             if (!item.getTitle().equals(getString(R.string.more))) { | ||||||
|                 // do not change title for more fragment |                 // do not change title for more fragment | ||||||
|                 setTitle(item.getTitle()); |                 setTitle(item.getTitle()); | ||||||
|             } |             } | ||||||
|             Fragment fragment = NavTabLoggedOut.of(item.getOrder()).newInstance(); |             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 |         //showBottom so that we do not show the bottom tray again when constructing | ||||||
|         //from the saved instance state. |         //from the saved instance state. | ||||||
|         if (fragment instanceof ContributionsFragment) { |         if (fragment instanceof ContributionsFragment) { | ||||||
|  | @ -237,7 +240,8 @@ public class MainActivity  extends BaseActivity | ||||||
|             bookmarkFragment = (BookmarkFragment) fragment; |             bookmarkFragment = (BookmarkFragment) fragment; | ||||||
|             activeFragment = ActiveFragment.BOOKMARK; |             activeFragment = ActiveFragment.BOOKMARK; | ||||||
|         } else if (fragment == null && showBottom) { |         } 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(); |                 MoreBottomSheetLoggedOutFragment bottomSheet = new MoreBottomSheetLoggedOutFragment(); | ||||||
|                 bottomSheet.show(getSupportFragmentManager(), |                 bottomSheet.show(getSupportFragmentManager(), | ||||||
|                     "MoreBottomSheetLoggedOut"); |                     "MoreBottomSheetLoggedOut"); | ||||||
|  | @ -267,28 +271,30 @@ public class MainActivity  extends BaseActivity | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Adds number of uploads next to tab text "Contributions" then it will look like |      * Adds number of uploads next to tab text "Contributions" then it will look like "Contributions | ||||||
|      * "Contributions (NUMBER)" |      * (NUMBER)" | ||||||
|  |      * | ||||||
|      * @param uploadCount |      * @param uploadCount | ||||||
|      */ |      */ | ||||||
|     public void setNumOfUploads(int uploadCount) { |     public void setNumOfUploads(int uploadCount) { | ||||||
|         if (activeFragment == ActiveFragment.CONTRIBUTIONS) { |         if (activeFragment == ActiveFragment.CONTRIBUTIONS) { | ||||||
|             setTitle(getResources().getString(R.string.contributions_fragment) +" "+ ( |             setTitle(getResources().getString(R.string.contributions_fragment) + " " + ( | ||||||
|                 !(uploadCount == 0) ? |                 !(uploadCount == 0) ? | ||||||
|                 getResources() |                     getResources() | ||||||
|                 .getQuantityString(R.plurals.contributions_subtitle, |                         .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 |      * Resume the uploads that got stuck because of the app being killed or the device being | ||||||
|      * or the device being rebooted. |      * rebooted. | ||||||
|      * |      * <p> | ||||||
|      * When the app is terminated or the device is restarted, contributions remain in the |      * 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. |      * 'STATE_IN_PROGRESS' state. This status persists and doesn't change during these events. So, | ||||||
|      * So, retrieving contributions labeled as 'STATE_IN_PROGRESS' |      * retrieving contributions labeled as 'STATE_IN_PROGRESS' from the database will provide the | ||||||
|      * from the database will provide the list of uploads that appear as stuck on opening the app again |      * list of uploads that appear as stuck on opening the app again | ||||||
|      */ |      */ | ||||||
|     @SuppressLint("CheckResult") |     @SuppressLint("CheckResult") | ||||||
|     private void checkAndResumeStuckUploads() { |     private void checkAndResumeStuckUploads() { | ||||||
|  | @ -297,9 +303,10 @@ public class MainActivity  extends BaseActivity | ||||||
|             .subscribeOn(Schedulers.io()) |             .subscribeOn(Schedulers.io()) | ||||||
|             .blockingGet(); |             .blockingGet(); | ||||||
|         Timber.d("Resuming " + stuckUploads.size() + " uploads..."); |         Timber.d("Resuming " + stuckUploads.size() + " uploads..."); | ||||||
|         if(!stuckUploads.isEmpty()) { |         if (!stuckUploads.isEmpty()) { | ||||||
|             for(Contribution contribution: stuckUploads) { |             for (Contribution contribution : stuckUploads) { | ||||||
|                 contribution.setState(Contribution.STATE_QUEUED); |                 contribution.setState(Contribution.STATE_QUEUED); | ||||||
|  |                 contribution.setDateUploadStarted(Calendar.getInstance().getTime()); | ||||||
|                 Completable.fromAction(() -> contributionDao.saveSynchronous(contribution)) |                 Completable.fromAction(() -> contributionDao.saveSynchronous(contribution)) | ||||||
|                     .subscribeOn(Schedulers.io()) |                     .subscribeOn(Schedulers.io()) | ||||||
|                     .subscribe(); |                     .subscribe(); | ||||||
|  | @ -326,24 +333,24 @@ public class MainActivity  extends BaseActivity | ||||||
|     protected void onRestoreInstanceState(Bundle savedInstanceState) { |     protected void onRestoreInstanceState(Bundle savedInstanceState) { | ||||||
|         super.onRestoreInstanceState(savedInstanceState); |         super.onRestoreInstanceState(savedInstanceState); | ||||||
|         String activeFragmentName = savedInstanceState.getString("activeFragment"); |         String activeFragmentName = savedInstanceState.getString("activeFragment"); | ||||||
|         if(activeFragmentName != null) { |         if (activeFragmentName != null) { | ||||||
|             restoreActiveFragment(activeFragmentName); |             restoreActiveFragment(activeFragmentName); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private void restoreActiveFragment(@NonNull String fragmentName) { |     private void restoreActiveFragment(@NonNull String fragmentName) { | ||||||
|         if(fragmentName.equals(ActiveFragment.CONTRIBUTIONS.name())) { |         if (fragmentName.equals(ActiveFragment.CONTRIBUTIONS.name())) { | ||||||
|             setTitle(getString(R.string.contributions_fragment)); |             setTitle(getString(R.string.contributions_fragment)); | ||||||
|             loadFragment(ContributionsFragment.newInstance(),false); |             loadFragment(ContributionsFragment.newInstance(), false); | ||||||
|         }else if(fragmentName.equals(ActiveFragment.NEARBY.name())) { |         } else if (fragmentName.equals(ActiveFragment.NEARBY.name())) { | ||||||
|             setTitle(getString(R.string.nearby_fragment)); |             setTitle(getString(R.string.nearby_fragment)); | ||||||
|             loadFragment(NearbyParentFragment.newInstance(),false); |             loadFragment(NearbyParentFragment.newInstance(), false); | ||||||
|         }else if(fragmentName.equals(ActiveFragment.EXPLORE.name())) { |         } else if (fragmentName.equals(ActiveFragment.EXPLORE.name())) { | ||||||
|             setTitle(getString(R.string.navigation_item_explore)); |             setTitle(getString(R.string.navigation_item_explore)); | ||||||
|             loadFragment(ExploreFragment.newInstance(),false); |             loadFragment(ExploreFragment.newInstance(), false); | ||||||
|         }else if(fragmentName.equals(ActiveFragment.BOOKMARK.name())) { |         } else if (fragmentName.equals(ActiveFragment.BOOKMARK.name())) { | ||||||
|             setTitle(getString(R.string.bookmarks)); |             setTitle(getString(R.string.bookmarks)); | ||||||
|             loadFragment(BookmarkFragment.newInstance(),false); |             loadFragment(BookmarkFragment.newInstance(), false); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -359,8 +366,9 @@ public class MainActivity  extends BaseActivity | ||||||
|             // Means that nearby fragment is visible |             // Means that nearby fragment is visible | ||||||
|             /* If function nearbyParentFragment.backButtonClick() returns false, it means that the bottomsheet is |             /* 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 */ |               not expanded. So if the back button is pressed, then go back to the Contributions tab */ | ||||||
|             if(!nearbyParentFragment.backButtonClicked()){ |             if (!nearbyParentFragment.backButtonClicked()) { | ||||||
|                 getSupportFragmentManager().beginTransaction().remove(nearbyParentFragment).commit(); |                 getSupportFragmentManager().beginTransaction().remove(nearbyParentFragment) | ||||||
|  |                     .commit(); | ||||||
|                 setSelectedItemId(NavTab.CONTRIBUTIONS.code()); |                 setSelectedItemId(NavTab.CONTRIBUTIONS.code()); | ||||||
|             } |             } | ||||||
|         } else if (exploreFragment != null && activeFragment == ActiveFragment.EXPLORE) { |         } else if (exploreFragment != null && activeFragment == ActiveFragment.EXPLORE) { | ||||||
|  | @ -385,18 +393,6 @@ public class MainActivity  extends BaseActivity | ||||||
|         //initBackButton(); |         //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 |      * Retry all failed uploads as soon as the user returns to the app | ||||||
|      */ |      */ | ||||||
|  | @ -406,41 +402,45 @@ public class MainActivity  extends BaseActivity | ||||||
|             getContribution(Collections.singletonList(Contribution.STATE_FAILED)) |             getContribution(Collections.singletonList(Contribution.STATE_FAILED)) | ||||||
|             .subscribeOn(Schedulers.io()) |             .subscribeOn(Schedulers.io()) | ||||||
|             .subscribe(failedUploads -> { |             .subscribe(failedUploads -> { | ||||||
|                 for (Contribution contribution: failedUploads) { |                 for (Contribution contribution : failedUploads) { | ||||||
|                     contributionsFragment.retryUpload(contribution); |                     contributionsFragment.retryUpload(contribution); | ||||||
|                 } |                 } | ||||||
|             }); |             }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public void toggleLimitedConnectionMode() { |     /** | ||||||
|         defaultKvStore.putBoolean(CommonsApplication.IS_LIMITED_CONNECTION_MODE_ENABLED, |      * Handles item selection in the options menu. This method is called when a user interacts with | ||||||
|             !defaultKvStore |      * the options menu in the Top Bar. | ||||||
|                 .getBoolean(CommonsApplication.IS_LIMITED_CONNECTION_MODE_ENABLED, false)); |      */ | ||||||
|         if (defaultKvStore |     @Override | ||||||
|             .getBoolean(CommonsApplication.IS_LIMITED_CONNECTION_MODE_ENABLED, false)) { |     public boolean onOptionsItemSelected(MenuItem item) { | ||||||
|             viewUtilWrapper |         switch (item.getItemId()) { | ||||||
|                 .showShortToast(getBaseContext(), getString(R.string.limited_connection_enabled)); |             case R.id.upload_tab: | ||||||
|         } else { |                 startActivity(new Intent(this, UploadProgressActivity.class)); | ||||||
|             WorkRequestHelper.Companion.makeOneTimeWorkRequest(getApplicationContext(), |                 return true; | ||||||
|                 ExistingWorkPolicy.APPEND_OR_REPLACE); |             case R.id.notifications: | ||||||
|             viewUtilWrapper |                 // Starts notification activity on click to notification icon | ||||||
|                 .showShortToast(getBaseContext(), getString(R.string.limited_connection_disabled)); |                 NotificationActivity.startYourself(this, "unread"); | ||||||
|  |                 return true; | ||||||
|  |             default: | ||||||
|  |                 return super.onOptionsItemSelected(item); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public void centerMapToPlace(Place place) { |     public void centerMapToPlace(Place place) { | ||||||
|         setSelectedItemId(NavTab.NEARBY.code()); |         setSelectedItemId(NavTab.NEARBY.code()); | ||||||
|         nearbyParentFragment.setNearbyParentFragmentInstanceReadyCallback(new NearbyParentFragmentInstanceReadyCallback() { |         nearbyParentFragment.setNearbyParentFragmentInstanceReadyCallback( | ||||||
|             @Override |             new NearbyParentFragmentInstanceReadyCallback() { | ||||||
|             public void onReady() { |                 @Override | ||||||
|                 nearbyParentFragment.centerMapToPlace(place); |                 public void onReady() { | ||||||
|             } |                     nearbyParentFragment.centerMapToPlace(place); | ||||||
|         }); |                 } | ||||||
|  |             }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|     protected void onActivityResult(int requestCode, int resultCode, Intent data) { |     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); |         super.onActivityResult(requestCode, resultCode, data); | ||||||
|         controller.handleActivityResult(this, requestCode, resultCode, data); |         controller.handleActivityResult(this, requestCode, resultCode, data); | ||||||
|     } |     } | ||||||
|  | @ -485,14 +485,15 @@ public class MainActivity  extends BaseActivity | ||||||
|     /** |     /** | ||||||
|      * Load default language in onCreate from SharedPreferences |      * Load default language in onCreate from SharedPreferences | ||||||
|      */ |      */ | ||||||
|     private void loadLocale(){ |     private void loadLocale() { | ||||||
|         final SharedPreferences preferences = getSharedPreferences("Settings", Activity.MODE_PRIVATE); |         final SharedPreferences preferences = getSharedPreferences("Settings", | ||||||
|  |             Activity.MODE_PRIVATE); | ||||||
|         final String language = preferences.getString("language", ""); |         final String language = preferences.getString("language", ""); | ||||||
|         final SettingsFragment settingsFragment = new SettingsFragment(); |         final SettingsFragment settingsFragment = new SettingsFragment(); | ||||||
|         settingsFragment.setLocale(this, language); |         settingsFragment.setLocale(this, language); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public NavTabLayout.OnNavigationItemSelectedListener getNavListener(){ |     public NavTabLayout.OnNavigationItemSelectedListener getNavListener() { | ||||||
|         return navListener; |         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.review.ReviewActivity; | ||||||
| import fr.free.nrw.commons.settings.SettingsActivity; | import fr.free.nrw.commons.settings.SettingsActivity; | ||||||
| import fr.free.nrw.commons.upload.UploadActivity; | import fr.free.nrw.commons.upload.UploadActivity; | ||||||
|  | import fr.free.nrw.commons.upload.UploadProgressActivity; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * This Class handles the dependency injection (using dagger) |  * This Class handles the dependency injection (using dagger) | ||||||
|  | @ -81,6 +82,9 @@ public abstract class ActivityBuilderModule { | ||||||
|     @ContributesAndroidInjector |     @ContributesAndroidInjector | ||||||
|     abstract ZoomableActivity bindZoomableActivity(); |     abstract ZoomableActivity bindZoomableActivity(); | ||||||
| 
 | 
 | ||||||
|  |     @ContributesAndroidInjector | ||||||
|  |     abstract UploadProgressActivity bindUploadProgressActivity(); | ||||||
|  | 
 | ||||||
|     @ContributesAndroidInjector |     @ContributesAndroidInjector | ||||||
|     abstract WikidataFeedback bindWikiFeedback(); |     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.profile.leaderboard.LeaderboardFragment; | ||||||
| import fr.free.nrw.commons.review.ReviewImageFragment; | import fr.free.nrw.commons.review.ReviewImageFragment; | ||||||
| import fr.free.nrw.commons.settings.SettingsFragment; | 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.categories.UploadCategoriesFragment; | ||||||
| import fr.free.nrw.commons.upload.depicts.DepictsFragment; | import fr.free.nrw.commons.upload.depicts.DepictsFragment; | ||||||
| import fr.free.nrw.commons.upload.license.MediaLicenseFragment; | import fr.free.nrw.commons.upload.license.MediaLicenseFragment; | ||||||
|  | @ -155,4 +157,10 @@ public abstract class FragmentBuilderModule { | ||||||
| 
 | 
 | ||||||
|     @ContributesAndroidInjector |     @ContributesAndroidInjector | ||||||
|     abstract LeaderboardFragment bindLeaderboardFragment(); |     abstract LeaderboardFragment bindLeaderboardFragment(); | ||||||
|  | 
 | ||||||
|  |     @ContributesAndroidInjector | ||||||
|  |     abstract PendingUploadsFragment bindPendingUploadsFragment(); | ||||||
|  | 
 | ||||||
|  |     @ContributesAndroidInjector | ||||||
|  |     abstract FailedUploadsFragment bindFailedUploadsFragment(); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -306,7 +306,6 @@ public class AchievementsFragment extends CommonsDaggerSupportFragment { | ||||||
|         if (uploadCount==0){ |         if (uploadCount==0){ | ||||||
|             setZeroAchievements(); |             setZeroAchievements(); | ||||||
|         }else { |         }else { | ||||||
| 
 |  | ||||||
|             binding.imagesUploadedProgressbar.setVisibility(View.VISIBLE); |             binding.imagesUploadedProgressbar.setVisibility(View.VISIBLE); | ||||||
|             binding.imagesUploadedProgressbar.setProgress |             binding.imagesUploadedProgressbar.setProgress | ||||||
|                     (100*uploadCount/levelInfo.getMaxUploadCount()); |                     (100*uploadCount/levelInfo.getMaxUploadCount()); | ||||||
|  | @ -326,9 +325,9 @@ public class AchievementsFragment extends CommonsDaggerSupportFragment { | ||||||
|             getString(R.string.ok), |             getString(R.string.ok), | ||||||
|             () -> {}, |             () -> {}, | ||||||
|             true); |             true); | ||||||
|         binding.imagesUploadedProgressbar.setVisibility(View.INVISIBLE); | //        binding.imagesUploadedProgressbar.setVisibility(View.INVISIBLE); | ||||||
|         binding.imageRevertsProgressbar.setVisibility(View.INVISIBLE); | //        binding.imageRevertsProgressbar.setVisibility(View.INVISIBLE); | ||||||
|         binding.imagesUsedByWikiProgressBar.setVisibility(View.INVISIBLE); | //        binding.imagesUsedByWikiProgressBar.setVisibility(View.INVISIBLE); | ||||||
|         binding.achievementBadgeImage.setVisibility(View.INVISIBLE); |         binding.achievementBadgeImage.setVisibility(View.INVISIBLE); | ||||||
|         binding.imagesUsedByWikiText.setText(R.string.no_image); |         binding.imagesUsedByWikiText.setText(R.string.no_image); | ||||||
|         binding.imagesRevertedText.setText(R.string.no_image_reverted); |         binding.imagesRevertedText.setText(R.string.no_image_reverted); | ||||||
|  | @ -354,7 +353,7 @@ public class AchievementsFragment extends CommonsDaggerSupportFragment { | ||||||
|      * @param achievements |      * @param achievements | ||||||
|      */ |      */ | ||||||
|     private void inflateAchievements(Achievements achievements) { |     private void inflateAchievements(Achievements achievements) { | ||||||
|         binding.imagesUsedByWikiProgressBar.setVisibility(View.VISIBLE); | //        binding.imagesUsedByWikiProgressBar.setVisibility(View.VISIBLE); | ||||||
|         binding.thanksReceived.setText(String.valueOf(achievements.getThanksReceived())); |         binding.thanksReceived.setText(String.valueOf(achievements.getThanksReceived())); | ||||||
|         binding.imagesUsedByWikiProgressBar.setProgress |         binding.imagesUsedByWikiProgressBar.setProgress | ||||||
|                 (100 * achievements.getUniqueUsedImages() / levelInfo.getMaxUniqueImages()); |                 (100 * achievements.getUniqueUsedImages() / levelInfo.getMaxUniqueImages()); | ||||||
|  |  | ||||||
|  | @ -203,6 +203,16 @@ public class UploadRepository { | ||||||
|         return uploadModel.getImageQuality(uploadItem, location); |         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 |      * 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 |      * @param filePath file to be checked | ||||||
|      * @return IMAGE_DUPLICATE or IMAGE_OK |      * @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); |         Timber.d("Checking for duplicate image %s", filePath); | ||||||
|         return Single.fromCallable(() -> fileUtilsWrapper.getFileInputStream(filePath)) |         return Single.fromCallable(() -> fileUtilsWrapper.getFileInputStream(filePath)) | ||||||
|             .map(fileUtilsWrapper::getSHA1) |             .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 { | enum class StashUploadState { | ||||||
|     SUCCESS, |     SUCCESS, | ||||||
|     PAUSED, |     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.auth.csrf.CsrfTokenClient | ||||||
| import fr.free.nrw.commons.contributions.ChunkInfo | import fr.free.nrw.commons.contributions.ChunkInfo | ||||||
| import fr.free.nrw.commons.contributions.Contribution | 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.upload.worker.UploadWorker.NotificationUpdateProgressListener | ||||||
| import fr.free.nrw.commons.wikidata.mwapi.MwException | import fr.free.nrw.commons.wikidata.mwapi.MwException | ||||||
| import io.reactivex.Observable | import io.reactivex.Observable | ||||||
|  | @ -33,7 +34,8 @@ class UploadClient @Inject constructor( | ||||||
|     private val csrfTokenClient: CsrfTokenClient, |     private val csrfTokenClient: CsrfTokenClient, | ||||||
|     private val pageContentsCreator: PageContentsCreator, |     private val pageContentsCreator: PageContentsCreator, | ||||||
|     private val fileUtilsWrapper: FileUtilsWrapper, |     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 |     private val CHUNK_SIZE = 512 * 1024 // 512 KB | ||||||
| 
 | 
 | ||||||
|  | @ -58,8 +60,6 @@ class UploadClient @Inject constructor( | ||||||
|             ) |             ) | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         contribution.unpause() |  | ||||||
| 
 |  | ||||||
|         val file = contribution.localUriPath |         val file = contribution.localUriPath | ||||||
|         val fileChunks = fileUtilsWrapper.getFileChunks(file, CHUNK_SIZE) |         val fileChunks = fileUtilsWrapper.getFileChunks(file, CHUNK_SIZE) | ||||||
|         val mediaType = fileUtilsWrapper.getMimeType(file).toMediaTypeOrNull() |         val mediaType = fileUtilsWrapper.getMimeType(file).toMediaTypeOrNull() | ||||||
|  | @ -79,17 +79,35 @@ class UploadClient @Inject constructor( | ||||||
|         val errorMessage = AtomicReference<String>() |         val errorMessage = AtomicReference<String>() | ||||||
|         compositeDisposable.add( |         compositeDisposable.add( | ||||||
|             Observable.fromIterable(fileChunks).forEach { chunkFile: File -> |             Observable.fromIterable(fileChunks).forEach { chunkFile: File -> | ||||||
|                 if (canProcess(contribution, failures)) { |                 if (canProcess(contributionDao, contribution, failures)) { | ||||||
|                     processChunk( |                     if (contributionDao.getContribution(contribution.pageId) == null) { | ||||||
|                         filename, contribution, notificationUpdater, chunkFile, |                         compositeDisposable.clear() | ||||||
|                         failures, chunkInfo, index, errorMessage, mediaType!!, file!!, fileChunks.size |                         return@forEach | ||||||
|                     ) |                     } else { | ||||||
|  |                         processChunk( | ||||||
|  |                             filename, | ||||||
|  |                             contribution, | ||||||
|  |                             notificationUpdater, | ||||||
|  |                             chunkFile, | ||||||
|  |                             failures, | ||||||
|  |                             chunkInfo, | ||||||
|  |                             index, | ||||||
|  |                             errorMessage, | ||||||
|  |                             mediaType!!, | ||||||
|  |                             file!!, | ||||||
|  |                             fileChunks.size | ||||||
|  |                         ) | ||||||
|  |                     } | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|         return when { |         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) |                 Timber.d("Upload stash paused %s", contribution.pageId) | ||||||
|                 Observable.just(StashUploadResult(StashUploadState.PAUSED, null, null)) |                 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, |     // As long as the contribution hasn't been paused and there are no errors, | ||||||
|     // we can process the current chunk. |     // 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( | private fun shouldSkip( | ||||||
|  |  | ||||||
|  | @ -103,6 +103,16 @@ public class UploadModel { | ||||||
|         return imageProcessingService.validateImage(uploadItem, inAppPictureLocation); |         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 |      * Calls validateCaption() of ImageProcessingService to check caption of image | ||||||
|      * |      * | ||||||
|  |  | ||||||
|  | @ -5,6 +5,7 @@ import dagger.Binds; | ||||||
| import dagger.Module; | import dagger.Module; | ||||||
| import dagger.Provides; | import dagger.Provides; | ||||||
| import fr.free.nrw.commons.auth.csrf.CsrfTokenClient; | 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.di.NetworkingModule; | ||||||
| import fr.free.nrw.commons.upload.categories.CategoriesContract; | import fr.free.nrw.commons.upload.categories.CategoriesContract; | ||||||
| import fr.free.nrw.commons.upload.categories.CategoriesPresenter; | import fr.free.nrw.commons.upload.categories.CategoriesPresenter; | ||||||
|  | @ -50,8 +51,8 @@ public abstract class UploadModule { | ||||||
|     public static UploadClient provideUploadClient(final UploadInterface uploadInterface, |     public static UploadClient provideUploadClient(final UploadInterface uploadInterface, | ||||||
|         @Named(NetworkingModule.NAMED_COMMONS_CSRF) final CsrfTokenClient csrfTokenClient, |         @Named(NetworkingModule.NAMED_COMMONS_CSRF) final CsrfTokenClient csrfTokenClient, | ||||||
|         final PageContentsCreator pageContentsCreator, final FileUtilsWrapper fileUtilsWrapper, |         final PageContentsCreator pageContentsCreator, final FileUtilsWrapper fileUtilsWrapper, | ||||||
|         final Gson gson) { |         final Gson gson, final ContributionDao contributionDao) { | ||||||
|         return new UploadClient(uploadInterface, csrfTokenClient, pageContentsCreator, |         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) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -35,13 +35,11 @@ import fr.free.nrw.commons.upload.FileUtilsWrapper | ||||||
| import fr.free.nrw.commons.upload.StashUploadResult | import fr.free.nrw.commons.upload.StashUploadResult | ||||||
| import fr.free.nrw.commons.upload.StashUploadState | import fr.free.nrw.commons.upload.StashUploadState | ||||||
| import fr.free.nrw.commons.upload.UploadClient | 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.upload.UploadResult | ||||||
| import fr.free.nrw.commons.wikidata.WikidataEditService | import fr.free.nrw.commons.wikidata.WikidataEditService | ||||||
| import kotlinx.coroutines.Dispatchers | import kotlinx.coroutines.Dispatchers | ||||||
| import kotlinx.coroutines.MainScope | 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.launch | ||||||
| import kotlinx.coroutines.withContext | import kotlinx.coroutines.withContext | ||||||
| import timber.log.Timber | import timber.log.Timber | ||||||
|  | @ -105,7 +103,6 @@ class UploadWorker( | ||||||
|             getNotificationBuilder(CommonsApplication.NOTIFICATION_CHANNEL_ID_ALL)!! |             getNotificationBuilder(CommonsApplication.NOTIFICATION_CHANNEL_ID_ALL)!! | ||||||
| 
 | 
 | ||||||
|         statesToProcess.add(Contribution.STATE_QUEUED) |         statesToProcess.add(Contribution.STATE_QUEUED) | ||||||
|         statesToProcess.add(Contribution.STATE_QUEUED_LIMITED_CONNECTION_MODE) |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @dagger.Module |     @dagger.Module | ||||||
|  | @ -167,105 +164,85 @@ class UploadWorker( | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     override suspend fun doWork(): Result { |     override suspend fun doWork(): Result { | ||||||
|         var countUpload = 0 |         try { | ||||||
|         // Start a foreground service |             var totalUploadsStarted = 0 | ||||||
|         setForeground(createForegroundInfo()) |             // Start a foreground service | ||||||
|         notificationManager = NotificationManagerCompat.from(appContext) |             setForeground(createForegroundInfo()) | ||||||
|         val processingUploads = getNotificationBuilder( |             notificationManager = NotificationManagerCompat.from(appContext) | ||||||
|             CommonsApplication.NOTIFICATION_CHANNEL_ID_ALL |             val processingUploads = getNotificationBuilder( | ||||||
|         )!! |                 CommonsApplication.NOTIFICATION_CHANNEL_ID_ALL | ||||||
|         withContext(Dispatchers.IO) { |             )!! | ||||||
|             /* |             withContext(Dispatchers.IO) { | ||||||
|                 queuedContributions receives the results from a one-shot query. |                 while (contributionDao.getContribution(statesToProcess) | ||||||
|                 This means that once the list has been fetched from the database, |                         .blockingGet().size > 0 && contributionDao.getContribution( | ||||||
|                 it does not get updated even if some changes (insertions, deletions, etc.) |                         arrayListOf( | ||||||
|                 are made to the contribution table afterwards. |                             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, | ||||||
|  |                     it does not get updated even if some changes (insertions, deletions, etc.) | ||||||
|  |                     are made to the contribution table afterwards. | ||||||
| 
 | 
 | ||||||
|                 Related issues (fixed): |                     Related issues (fixed): | ||||||
|                 https://github.com/commons-app/apps-android-commons/issues/5136 |                     https://github.com/commons-app/apps-android-commons/issues/5136 | ||||||
|                 https://github.com/commons-app/apps-android-commons/issues/5346 |                     https://github.com/commons-app/apps-android-commons/issues/5346 | ||||||
|              */ |                  */ | ||||||
|             val queuedContributions = contributionDao.getContribution(statesToProcess) |                     val queuedContributions = contributionDao.getContribution(statesToProcess) | ||||||
|                 .blockingGet() |                         .blockingGet() | ||||||
|             //Showing initial notification for the number of uploads being processed |                     //Showing initial notification for the number of uploads being processed | ||||||
| 
 | 
 | ||||||
|             Timber.e("Queued Contributions: %s", queuedContributions.size) |                     processingUploads.setContentTitle(appContext.getString(R.string.starting_uploads)) | ||||||
| 
 |                     processingUploads.setContentText( | ||||||
|             processingUploads.setContentTitle(appContext.getString(R.string.starting_uploads)) |                         appContext.resources.getQuantityString( | ||||||
|             processingUploads.setContentText( |                             R.plurals.starting_multiple_uploads, | ||||||
|                 appContext.resources.getQuantityString( |                             queuedContributions.size, | ||||||
|                         R.plurals.starting_multiple_uploads, |                             queuedContributions.size | ||||||
|                         queuedContributions.size, |                         ) | ||||||
|                         queuedContributions.size |                     ) | ||||||
|  |                     notificationManager?.notify( | ||||||
|  |                         PROCESSING_UPLOADS_NOTIFICATION_TAG, | ||||||
|  |                         PROCESSING_UPLOADS_NOTIFICATION_ID, | ||||||
|  |                         processingUploads.build() | ||||||
|                     ) |                     ) | ||||||
|             ) |  | ||||||
|             notificationManager?.notify( |  | ||||||
|                 PROCESSING_UPLOADS_NOTIFICATION_TAG, |  | ||||||
|                 PROCESSING_UPLOADS_NOTIFICATION_ID, |  | ||||||
|                 processingUploads.build() |  | ||||||
|             ) |  | ||||||
| 
 | 
 | ||||||
|             /** |                     val sortedQueuedContributionsList: List<Contribution> = | ||||||
|              * To avoid race condition when multiple of these workers are working, assign this state |                         queuedContributions.sortedBy { it.dateUploadStartedInMillis() } | ||||||
|             so that the next one does not process these contribution again |  | ||||||
|              */ |  | ||||||
|             queuedContributions.forEach { |  | ||||||
|                 it.state = Contribution.STATE_IN_PROGRESS |  | ||||||
|                 contributionDao.saveSynchronous(it) |  | ||||||
|             } |  | ||||||
| 
 | 
 | ||||||
|             queuedContributions.asFlow().map { contribution -> |                     var contribution = sortedQueuedContributionsList.first() | ||||||
|                 // Upload the contribution if it has not been cancelled by the user | 
 | ||||||
|                 if (!CommonsApplication.cancelledUploads.contains(contribution.pageId)) { |                     if (contributionDao.getContribution(contribution.pageId) != null) { | ||||||
|                     /** |  | ||||||
|                      * 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 { |  | ||||||
|                         contribution.transferred = 0 |                         contribution.transferred = 0 | ||||||
|                         contribution.state = Contribution.STATE_IN_PROGRESS |                         contribution.state = Contribution.STATE_IN_PROGRESS | ||||||
|                         contributionDao.saveSynchronous(contribution) |                         contributionDao.saveSynchronous(contribution) | ||||||
|                         setProgressAsync(Data.Builder().putInt("progress", countUpload).build()) |                         setProgressAsync(Data.Builder().putInt("progress", totalUploadsStarted).build()) | ||||||
|                         countUpload++ |                         totalUploadsStarted++ | ||||||
|                         uploadContribution(contribution = contribution) |                         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, | ||||||
|  |                     PROCESSING_UPLOADS_NOTIFICATION_ID | ||||||
|  |                 ) | ||||||
|  |             } | ||||||
|  |             // Trigger WorkManager to process any new contributions that may have been added to the queue | ||||||
|  |             val updatedContributionQueue = withContext(Dispatchers.IO) { | ||||||
|  |                 contributionDao.getContribution(statesToProcess).blockingGet() | ||||||
|  |             } | ||||||
|  |             if (updatedContributionQueue.isNotEmpty()) { | ||||||
|  |                 return Result.retry() | ||||||
|  |             } | ||||||
| 
 | 
 | ||||||
|             //Dismiss the global notification |             return Result.success() | ||||||
|             notificationManager?.cancel( |         } catch (e: Exception) { | ||||||
|                 PROCESSING_UPLOADS_NOTIFICATION_TAG, |             Timber.e(e, "UploadWorker encountered an error.") | ||||||
|                 PROCESSING_UPLOADS_NOTIFICATION_ID |             return Result.failure() | ||||||
|             ) |         } finally { | ||||||
|  |             WorkRequestHelper.markUploadWorkerAsStopped() | ||||||
|         } |         } | ||||||
|         //Trigger WorkManager to process any new contributions that may have been added to the queue |  | ||||||
|         val updatedContributionQueue = withContext(Dispatchers.IO) { |  | ||||||
|             contributionDao.getContribution(statesToProcess).blockingGet() |  | ||||||
|         } |  | ||||||
|         if (updatedContributionQueue.isNotEmpty()) { |  | ||||||
|             return Result.retry() |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         return Result.success() |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Removes the processed contribution from the cancelledUploads in-memory hashset |  | ||||||
|      */ |  | ||||||
|     private fun removeUploadFromInMemoryHashSet(contribution: Contribution) { |  | ||||||
|         CommonsApplication.cancelledUploads.remove(contribution.pageId) |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  | @ -296,12 +273,6 @@ class UploadWorker( | ||||||
|             .setContentTitle(appContext.getString(R.string.upload_in_progress)) |             .setContentTitle(appContext.getString(R.string.upload_in_progress)) | ||||||
|             .build() |             .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 |      * Upload the contribution | ||||||
|  | @ -354,7 +325,6 @@ class UploadWorker( | ||||||
|                     StashUploadState.FAILED,fileKey = null,errorMessage = it.message |                     StashUploadState.FAILED,fileKey = null,errorMessage = it.message | ||||||
|                 ) |                 ) | ||||||
|             }.blockingSingle() |             }.blockingSingle() | ||||||
| 
 |  | ||||||
|             when (stashUploadResult.state) { |             when (stashUploadResult.state) { | ||||||
|                 StashUploadState.SUCCESS -> { |                 StashUploadState.SUCCESS -> { | ||||||
|                     //If the stash upload succeeds |                     //If the stash upload succeeds | ||||||
|  | @ -414,16 +384,22 @@ class UploadWorker( | ||||||
|                     contribution.state = Contribution.STATE_PAUSED |                     contribution.state = Contribution.STATE_PAUSED | ||||||
|                     contributionDao.saveSynchronous(contribution) |                     contributionDao.saveSynchronous(contribution) | ||||||
|                 } |                 } | ||||||
|  |                 StashUploadState.CANCELLED -> { | ||||||
|  |                     showCancelledNotification(contribution) | ||||||
|  |                 } | ||||||
|                 else -> { |                 else -> { | ||||||
|                     Timber.e("Upload file to stash failed with status: ${stashUploadResult.state}") |                     Timber.e("""upload file to stash failed with status: ${stashUploadResult.state}""") | ||||||
|                     showInvalidLoginNotification(contribution) | 
 | ||||||
|                     contribution.state = Contribution.STATE_FAILED |                     contribution.state = Contribution.STATE_FAILED | ||||||
|                     contribution.chunkInfo = null |                     contribution.chunkInfo = null | ||||||
|  |                     contribution.errorInfo = stashUploadResult.errorMessage | ||||||
|  |                     showErrorNotification(contribution) | ||||||
|                     contributionDao.saveSynchronous(contribution) |                     contributionDao.saveSynchronous(contribution) | ||||||
|                     if (stashUploadResult.errorMessage.equals( |                     if (stashUploadResult.errorMessage.equals( | ||||||
|                             CsrfTokenClient.INVALID_TOKEN_ERROR_MESSAGE) |                             CsrfTokenClient.INVALID_TOKEN_ERROR_MESSAGE) | ||||||
|                         ) { |                         ) { | ||||||
|                         Timber.e("Invalid Login, logging out") |                         Timber.e("Invalid Login, logging out") | ||||||
|  |                         showInvalidLoginNotification(contribution) | ||||||
|                         val username = sessionManager.userName |                         val username = sessionManager.userName | ||||||
|                         var logoutListener = CommonsApplication.BaseLogoutListener( |                         var logoutListener = CommonsApplication.BaseLogoutListener( | ||||||
|                             appContext, |                             appContext, | ||||||
|  | @ -439,6 +415,7 @@ class UploadWorker( | ||||||
|             Timber.e(exception) |             Timber.e(exception) | ||||||
|             Timber.e("Stash upload failed for contribution: $filename") |             Timber.e("Stash upload failed for contribution: $filename") | ||||||
|             showFailedNotification(contribution) |             showFailedNotification(contribution) | ||||||
|  |             contribution.errorInfo=exception.message | ||||||
|             contribution.state=Contribution.STATE_FAILED |             contribution.state=Contribution.STATE_FAILED | ||||||
|             clearChunks(contribution) |             clearChunks(contribution) | ||||||
|         } |         } | ||||||
|  | @ -556,7 +533,8 @@ class UploadWorker( | ||||||
|     private fun showSuccessNotification(contribution: Contribution) { |     private fun showSuccessNotification(contribution: Contribution) { | ||||||
|         val displayTitle = contribution.media.displayTitle |         val displayTitle = contribution.media.displayTitle | ||||||
|         contribution.state=Contribution.STATE_COMPLETED |         contribution.state=Contribution.STATE_COMPLETED | ||||||
|         currentNotification.setContentTitle( |         curentNotification.setContentIntent(getPendingIntent(MainActivity::class.java)) | ||||||
|  |         curentNotification.setContentTitle( | ||||||
|             appContext.getString( |             appContext.getString( | ||||||
|                 R.string.upload_completed_notification_title, |                 R.string.upload_completed_notification_title, | ||||||
|                 displayTitle |                 displayTitle | ||||||
|  | @ -578,8 +556,8 @@ class UploadWorker( | ||||||
|     @SuppressLint("StringFormatInvalid") |     @SuppressLint("StringFormatInvalid") | ||||||
|     private fun showFailedNotification(contribution: Contribution) { |     private fun showFailedNotification(contribution: Contribution) { | ||||||
|         val displayTitle = contribution.media.displayTitle |         val displayTitle = contribution.media.displayTitle | ||||||
|         currentNotification.setContentIntent(getPendingIntent(MainActivity::class.java)) |         curentNotification.setContentIntent(getPendingIntent(UploadProgressActivity::class.java)) | ||||||
|         currentNotification.setContentTitle( |         curentNotification.setContentTitle( | ||||||
|             appContext.getString( |             appContext.getString( | ||||||
|                 R.string.upload_failed_notification_title, |                 R.string.upload_failed_notification_title, | ||||||
|                 displayTitle |                 displayTitle | ||||||
|  | @ -611,13 +589,36 @@ class UploadWorker( | ||||||
|         ) |         ) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     /** | ||||||
|  |      * 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 |      * Notify that the current upload is paused | ||||||
|      * @param contribution |      * @param contribution | ||||||
|      */ |      */ | ||||||
|     private fun showPausedNotification(contribution: Contribution) { |     private fun showPausedNotification(contribution: Contribution) { | ||||||
|         val displayTitle = contribution.media.displayTitle |         val displayTitle = contribution.media.displayTitle | ||||||
|         currentNotification.setContentTitle( |        | ||||||
|  |         curentNotification.setContentIntent(getPendingIntent(UploadProgressActivity::class.java)) | ||||||
|  |         curentNotification.setContentTitle( | ||||||
|             appContext.getString( |             appContext.getString( | ||||||
|                 R.string.upload_paused_notification_title, |                 R.string.upload_paused_notification_title, | ||||||
|                 displayTitle |                 displayTitle | ||||||
|  | @ -632,6 +633,25 @@ class UploadWorker( | ||||||
|         ) |         ) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     /** | ||||||
|  |      * 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 |      * Method used to get Pending intent for opening different screen after clicking on notification | ||||||
|      * @param toClass |      * @param toClass | ||||||
|  |  | ||||||
|  | @ -3,6 +3,7 @@ package fr.free.nrw.commons.upload.worker | ||||||
| import android.content.Context | import android.content.Context | ||||||
| import androidx.work.* | import androidx.work.* | ||||||
| import androidx.work.WorkRequest.Companion.MIN_BACKOFF_MILLIS | import androidx.work.WorkRequest.Companion.MIN_BACKOFF_MILLIS | ||||||
|  | import timber.log.Timber | ||||||
| import java.util.concurrent.TimeUnit | import java.util.concurrent.TimeUnit | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  | @ -11,7 +12,22 @@ import java.util.concurrent.TimeUnit | ||||||
| class WorkRequestHelper { | class WorkRequestHelper { | ||||||
| 
 | 
 | ||||||
|     companion object { |     companion object { | ||||||
|  | 
 | ||||||
|  |         private var isUploadWorkerRunning = false | ||||||
|  |         private val lock = Object() | ||||||
|  | 
 | ||||||
|         fun makeOneTimeWorkRequest(context: Context, existingWorkPolicy: ExistingWorkPolicy) { |         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 |             /* Set backoff criteria for the work request | ||||||
|            The default backoff policy is EXPONENTIAL, but while testing we found that it |            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 |            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( |             WorkManager.getInstance(context).enqueueUniqueWork( | ||||||
|                 UploadWorker::class.java.simpleName, existingWorkPolicy, uploadRequest |                 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"> |     android:viewportWidth="24.0"> | ||||||
| 
 | 
 | ||||||
|     <path |     <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"/> |         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> | </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_marginTop="@dimen/miniscule_margin" | ||||||
|       android:layout_margin="@dimen/very_tiny_gap"/> |       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 |   <FrameLayout | ||||||
|     android:id="@+id/explore_container" |     android:id="@+id/explore_container" | ||||||
|     android:layout_width="match_parent" |     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:paddingTop="@dimen/standard_gap" | ||||||
|       android:visibility="visible"> |       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 |       <ImageButton | ||||||
|         android:id="@+id/wikipediaButton" |         android:id="@+id/wikipediaButton" | ||||||
|         android:layout_width="48dp" |         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" | <menu xmlns:android="http://schemas.android.com/apk/res/android" | ||||||
|     xmlns:app="http://schemas.android.com/apk/res-auto"> |   xmlns:app="http://schemas.android.com/apk/res-auto"> | ||||||
| 
 |   <item | ||||||
|     <item android:id="@+id/toggle_limited_connection_mode" |     android:id="@+id/upload_tab" | ||||||
|       android:title="@string/limited_connection_mode" |     android:title="Upload" | ||||||
|       app:showAsAction="always" |     app:actionLayout="@layout/pending_uploads_icon" | ||||||
|       android:checkable="true" |     app:showAsAction="ifRoom|withText" /> | ||||||
|       android:icon="@drawable/ic_baseline_cloud_queue_24" |   <item | ||||||
|       /> |     android:id="@+id/notifications" | ||||||
|     <item android:id="@+id/notifications" |     android:menuCategory="secondary" | ||||||
|         android:title="@string/notifications" |     android:title="@string/notifications" | ||||||
|         app:showAsAction="ifRoom|withText" |     app:actionLayout="@layout/notification_icon" | ||||||
|         android:menuCategory="secondary" |     app:showAsAction="ifRoom|withText" /> | ||||||
|         app:actionLayout="@layout/notification_icon" |  | ||||||
|         /> |  | ||||||
| </menu> | </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> | ||||||
|  | @ -258,7 +258,7 @@ | ||||||
|   <string name="navigation_item_feedback">Ανατροφοδότηση</string> |   <string name="navigation_item_feedback">Ανατροφοδότηση</string> | ||||||
|   <string name="navigation_item_feedback_github">Ανατροφοδότηση μέσω Github</string> |   <string name="navigation_item_feedback_github">Ανατροφοδότηση μέσω Github</string> | ||||||
|   <string name="navigation_item_logout">Αποσύνδεση</string> |   <string name="navigation_item_logout">Αποσύνδεση</string> | ||||||
|   <string name="navigation_item_info">Σεμινάριο</string> |   <string name="navigation_item_info">Οδηγίες</string> | ||||||
|   <string name="navigation_item_notification">Ειδοποιήσεις</string> |   <string name="navigation_item_notification">Ειδοποιήσεις</string> | ||||||
|   <string name="navigation_item_review">Έλεγχος</string> |   <string name="navigation_item_review">Έλεγχος</string> | ||||||
|   <string name="no_description_found">δε βρέθηκε περιγραφή</string> |   <string name="no_description_found">δε βρέθηκε περιγραφή</string> | ||||||
|  | @ -388,13 +388,13 @@ | ||||||
|   <string name="image_reverts">Εικόνες που δεν ανεστράφησαν</string> |   <string name="image_reverts">Εικόνες που δεν ανεστράφησαν</string> | ||||||
|   <string name="images_used_by_wiki">Εικόνες που χρησιμοποιήθηκαν</string> |   <string name="images_used_by_wiki">Εικόνες που χρησιμοποιήθηκαν</string> | ||||||
|   <string name="achievements_share_message">Μοιραστείτε τα επιτεύγματά σας με τους φίλους σας!</string> |   <string name="achievements_share_message">Μοιραστείτε τα επιτεύγματά σας με τους φίλους σας!</string> | ||||||
|   <string name="achievements_info_message">Το επίπεδό σας αυξάνεται όσο πληρείτε αυτές στις απαιτήσεις. Τα αντικείμενα στο τμήμα \"Στατιστικά\" δεν μετρούν στο επίπεδό σας.</string> |   <string name="achievements_info_message">Το επίπεδό σας αυξάνεται όσο πληρείτε αυτές στις απαιτήσεις. Τα αντικείμενα στο τμήμα «Στατιστικά» δε μετρούν στο επίπεδό σας.</string> | ||||||
|   <string name="achievements_revert_limit_message">ελάχιστο που απαιτείται:</string> |   <string name="achievements_revert_limit_message">ελάχιστο απαιτούμενο όριο:</string> | ||||||
|   <string name="images_uploaded_explanation">Ο αριθμός εικόνων που ανεβάσατε στα Κοινά, μέσω οποιουδήποτε λογισμικού ανεβάσματος</string> |   <string name="images_uploaded_explanation">Ο αριθμός των εικόνων που έχετε ανεβάσει στα Commons, μέσω οποιουδήποτε λογισμικού μεταφόρτωσης</string> | ||||||
|   <string name="images_reverted_explanation">Το ποσοστό εικόνων που ανεβάσατε στα Κοινά που δεν διεγράφη</string> |   <string name="images_reverted_explanation">Το ποσοστό εικόνων που ανεβάσατε στα Commons που δε διεγράφη</string> | ||||||
|   <string name="images_used_explanation">Ο αριθμός εικόνων που ανεβάσατε στα Κοινά που χρησιμοποιήθηκαν σε λήμματα του Wikimedia</string> |   <string name="images_used_explanation">Ο αριθμός των εικόνων που έχετε μεταφορτώσει στα Commons που χρησιμοποιήθηκαν σε άρθρα του Wikimedia</string> | ||||||
|   <string name="error_occurred">Συνέβη σφάλμα!</string> |   <string name="error_occurred">Παρουσιάστηκε σφάλμα!</string> | ||||||
|   <string name="notifications_channel_name_all">Ειδοποίηση Κοινών</string> |   <string name="notifications_channel_name_all">Ειδοποίηση Commons</string> | ||||||
|   <string name="preference_author_name_toggle">Χρησιμοποιήστε προσαρμοσμένο όνομα δημιουργού</string> |   <string name="preference_author_name_toggle">Χρησιμοποιήστε προσαρμοσμένο όνομα δημιουργού</string> | ||||||
|   <string name="preference_author_name_toggle_summary">Χρησιμοποιήστε ένα προσαρμοσμένο όνομα δημιουργού αντί για το όνομα χρήστη σας κατά τη μεταφόρτωση φωτογραφιών</string> |   <string name="preference_author_name_toggle_summary">Χρησιμοποιήστε ένα προσαρμοσμένο όνομα δημιουργού αντί για το όνομα χρήστη σας κατά τη μεταφόρτωση φωτογραφιών</string> | ||||||
|   <string name="preference_author_name">Προσαρμοσμένο όνομα δημιουργού</string> |   <string name="preference_author_name">Προσαρμοσμένο όνομα δημιουργού</string> | ||||||
|  |  | ||||||
|  | @ -34,8 +34,8 @@ | ||||||
|   <string name="logging_in_title">لاگ ان تھیندا پئے</string> |   <string name="logging_in_title">لاگ ان تھیندا پئے</string> | ||||||
|   <string name="logging_in_message">انتظار کرو۔۔۔</string> |   <string name="logging_in_message">انتظار کرو۔۔۔</string> | ||||||
|   <string name="updating_caption_message">انتظار کرو۔۔۔</string> |   <string name="updating_caption_message">انتظار کرو۔۔۔</string> | ||||||
|   <string name="login_success" fuzzy="true">لاگ ان کامیاب!</string> |   <string name="login_success">لاگ ان کامیاب!</string> | ||||||
|   <string name="login_failed" fuzzy="true">لاگ ان ناکام!</string> |   <string name="login_failed">لاگ ان ناکام!</string> | ||||||
|   <string name="upload_failed">فائل کائنی لبھی،ٻئی فائل کیتے کوشش کرو۔</string> |   <string name="upload_failed">فائل کائنی لبھی،ٻئی فائل کیتے کوشش کرو۔</string> | ||||||
|   <string name="authentication_failed" fuzzy="true">تصدیق ناکام، ولدا لاڳ ان تھیوو</string> |   <string name="authentication_failed" fuzzy="true">تصدیق ناکام، ولدا لاڳ ان تھیوو</string> | ||||||
|   <string name="uploading_started">اپ لوڈ شروع!</string> |   <string name="uploading_started">اپ لوڈ شروع!</string> | ||||||
|  | @ -60,7 +60,7 @@ | ||||||
|   <string name="share_title_hint">عنوان (ضروری ہے)</string> |   <string name="share_title_hint">عنوان (ضروری ہے)</string> | ||||||
|   <string name="share_description_hint">تفصیل</string> |   <string name="share_description_hint">تفصیل</string> | ||||||
|   <string name="share_caption_hint">عنوان</string> |   <string name="share_caption_hint">عنوان</string> | ||||||
|   <string name="login_failed_generic" fuzzy="true">لاگ ان ناکام</string> |   <string name="login_failed_generic">لاگ ان ناکام</string> | ||||||
|   <string name="share_upload_button">اپلوڈ</string> |   <string name="share_upload_button">اپلوڈ</string> | ||||||
|   <string name="multiple_share_base_title">ایں سیٹ دا ناں ݙسو</string> |   <string name="multiple_share_base_title">ایں سیٹ دا ناں ݙسو</string> | ||||||
|   <string name="provider_modifications">تبدیلیاں</string> |   <string name="provider_modifications">تبدیلیاں</string> | ||||||
|  |  | ||||||
|  | @ -42,6 +42,7 @@ | ||||||
|     <attr name="more_bottom_sheet_drawable_color" format="reference"/> |     <attr name="more_bottom_sheet_drawable_color" format="reference"/> | ||||||
|     <attr name="card_item_color" format="reference"/> |     <attr name="card_item_color" format="reference"/> | ||||||
|     <attr name="notification_icon_drawable" 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="notification_icon_text_color" format="reference"/> | ||||||
|     <attr name="toggle_theme" format="reference"/> |     <attr name="toggle_theme" format="reference"/> | ||||||
|     <attr name="contributionsListTextSecondary" 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="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="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="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> |   <string name="could_not_load_place_data">Could not load place data</string> | ||||||
| </resources> | </resources> | ||||||
|  |  | ||||||
|  | @ -48,6 +48,7 @@ | ||||||
|         <item name="more_bottom_sheet_style">@style/DarkMoreBottomSheetStyle</item> |         <item name="more_bottom_sheet_style">@style/DarkMoreBottomSheetStyle</item> | ||||||
|         <item name="more_bottom_sheet_drawable_color">@color/white</item> |         <item name="more_bottom_sheet_drawable_color">@color/white</item> | ||||||
|         <item name="card_item_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_drawable">@drawable/ic_notifications_white_24dp</item> | ||||||
|         <item name="notification_icon_text_color">@color/white</item> |         <item name="notification_icon_text_color">@color/white</item> | ||||||
|         <item name="toggle_theme">@style/SwitchThemeDark</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_style">@style/LightMoreBottomSheetStyle</item> | ||||||
|         <item name="more_bottom_sheet_drawable_color">@color/black</item> |         <item name="more_bottom_sheet_drawable_color">@color/black</item> | ||||||
|         <item name="card_item_color">@color/primaryDarkColor</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_drawable">@drawable/ic_notifications_blue_24dp</item> | ||||||
|         <item name="notification_icon_text_color">@color/primaryDarkColor</item> |         <item name="notification_icon_text_color">@color/primaryDarkColor</item> | ||||||
|         <item name="toggle_theme">@style/SwitchThemeLight</item> |         <item name="toggle_theme">@style/SwitchThemeLight</item> | ||||||
|  |  | ||||||
|  | @ -92,63 +92,6 @@ class ContributionViewHolderUnitTests { | ||||||
|         Assert.assertNotNull(contributionViewHolder) |         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 |     @Test | ||||||
|     @Throws(Exception::class) |     @Throws(Exception::class) | ||||||
|     fun testWikipediaButtonClicked() { |     fun testWikipediaButtonClicked() { | ||||||
|  | @ -161,18 +104,6 @@ class ContributionViewHolderUnitTests { | ||||||
|         contributionViewHolder.imageClicked() |         contributionViewHolder.imageClicked() | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Test |  | ||||||
|     @Throws(Exception::class) |  | ||||||
|     fun testDeleteUpload() { |  | ||||||
|         contributionViewHolder.deleteUpload() |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Test |  | ||||||
|     @Throws(Exception::class) |  | ||||||
|     fun testRetryUpload() { |  | ||||||
|         contributionViewHolder.retryUpload() |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Test |     @Test | ||||||
|     @Throws(Exception::class) |     @Throws(Exception::class) | ||||||
|     fun testChooseImageSource() { |     fun testChooseImageSource() { | ||||||
|  | @ -240,17 +171,6 @@ class ContributionViewHolderUnitTests { | ||||||
|         contributionViewHolder.init(0, contribution) |         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 |     @Test | ||||||
|     @Throws(Exception::class) |     @Throws(Exception::class) | ||||||
|     fun testInitCaseNonNull_STATE_IN_PROGRESS() { |     fun testInitCaseNonNull_STATE_IN_PROGRESS() { | ||||||
|  |  | ||||||
|  | @ -205,7 +205,6 @@ class ContributionsFragmentUnitTests { | ||||||
|         `when`(menu.findItem(anyInt())).thenReturn(menuItem) |         `when`(menu.findItem(anyInt())).thenReturn(menuItem) | ||||||
|         `when`(menuItem.actionView).thenReturn(notification) |         `when`(menuItem.actionView).thenReturn(notification) | ||||||
|         `when`(store.getBoolean(anyString(), anyBoolean())).thenReturn(true) |         `when`(store.getBoolean(anyString(), anyBoolean())).thenReturn(true) | ||||||
|         fragment.updateLimitedConnectionToggle(menu) |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Test |     @Test | ||||||
|  |  | ||||||
|  | @ -137,20 +137,6 @@ class ContributionsListFragmentUnitTests { | ||||||
|         method.invoke(fragment, contribution) |         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 |     @Test | ||||||
|     @Throws(Exception::class) |     @Throws(Exception::class) | ||||||
|     fun testAddImageToWikipedia() { |     fun testAddImageToWikipedia() { | ||||||
|  | @ -165,20 +151,6 @@ class ContributionsListFragmentUnitTests { | ||||||
|         fragment.openMediaDetail(0, true) |         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 |     @Test | ||||||
|     @Throws(Exception::class) |     @Throws(Exception::class) | ||||||
|     fun testOnViewStateRestored() { |     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 androidx.loader.content.Loader | ||||||
| import com.nhaarman.mockitokotlin2.verify | import com.nhaarman.mockitokotlin2.verify | ||||||
| import com.nhaarman.mockitokotlin2.whenever | import com.nhaarman.mockitokotlin2.whenever | ||||||
|  | import fr.free.nrw.commons.repository.UploadRepository | ||||||
| import io.reactivex.Completable | import io.reactivex.Completable | ||||||
| import io.reactivex.schedulers.TestScheduler | import io.reactivex.schedulers.TestScheduler | ||||||
| import org.junit.Before | import org.junit.Before | ||||||
|  | @ -24,6 +25,10 @@ import org.mockito.MockitoAnnotations | ||||||
| class ContributionsPresenterTest { | class ContributionsPresenterTest { | ||||||
|     @Mock |     @Mock | ||||||
|     internal lateinit var repository: ContributionsRepository |     internal lateinit var repository: ContributionsRepository | ||||||
|  | 
 | ||||||
|  |     @Mock | ||||||
|  |     internal lateinit var uploadRepository: UploadRepository | ||||||
|  | 
 | ||||||
|     @Mock |     @Mock | ||||||
|     internal lateinit var view: ContributionsContract.View |     internal lateinit var view: ContributionsContract.View | ||||||
| 
 | 
 | ||||||
|  | @ -37,9 +42,11 @@ class ContributionsPresenterTest { | ||||||
| 
 | 
 | ||||||
|     lateinit var liveData: LiveData<List<Contribution>> |     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 |      * initial setup | ||||||
|  | @ -48,35 +55,23 @@ class ContributionsPresenterTest { | ||||||
|     @Throws(Exception::class) |     @Throws(Exception::class) | ||||||
|     fun setUp() { |     fun setUp() { | ||||||
|         MockitoAnnotations.initMocks(this) |         MockitoAnnotations.initMocks(this) | ||||||
|         scheduler=TestScheduler() |         scheduler = TestScheduler() | ||||||
|         cursor = Mockito.mock(Cursor::class.java) |         cursor = Mockito.mock(Cursor::class.java) | ||||||
|         contribution = Mockito.mock(Contribution::class.java) |         contribution = Mockito.mock(Contribution::class.java) | ||||||
|         contributionsPresenter = ContributionsPresenter(repository, scheduler) |         contributionsPresenter = ContributionsPresenter(repository, uploadRepository, scheduler) | ||||||
|         loader = Mockito.mock(CursorLoader::class.java) |         loader = Mockito.mock(CursorLoader::class.java) | ||||||
|         contributionsPresenter.onAttachView(view) |         contributionsPresenter.onAttachView(view) | ||||||
|         liveData=MutableLiveData() |         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) |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Test fetch contribution with filename |      * Test fetch contribution with filename | ||||||
|      */ |      */ | ||||||
|     @Test |     @Test | ||||||
|     fun testGetContributionWithFileName(){ |     fun testGetContributionWithFileName() { | ||||||
|         contributionsPresenter.getContributionsWithTitle("ashish") |         contributionsPresenter.getContributionsWithTitle("ashish") | ||||||
|         verify(repository).getContributionWithFileName("ashish") |         verify(repository).getContributionWithFileName("ashish") | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
| } | } | ||||||
|  | @ -195,25 +195,6 @@ class MainActivityUnitTests { | ||||||
|         MainActivity.startYourself(mockContext) |         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 |     @Test | ||||||
|     @Throws(Exception::class) |     @Throws(Exception::class) | ||||||
|     fun testSetUpPager() { |     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.auth.csrf.CsrfTokenClient | ||||||
| import fr.free.nrw.commons.contributions.ChunkInfo | import fr.free.nrw.commons.contributions.ChunkInfo | ||||||
| import fr.free.nrw.commons.contributions.Contribution | 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.upload.UploadClient.TimeProvider | ||||||
| import fr.free.nrw.commons.wikidata.mwapi.MwException | import fr.free.nrw.commons.wikidata.mwapi.MwException | ||||||
| import fr.free.nrw.commons.wikidata.mwapi.MwServiceError | import fr.free.nrw.commons.wikidata.mwapi.MwServiceError | ||||||
| import io.reactivex.Observable | import io.reactivex.Observable | ||||||
| import junit.framework.TestCase.assertEquals | import junit.framework.TestCase.assertEquals | ||||||
| import junit.framework.TestCase.assertSame | import junit.framework.TestCase.assertSame | ||||||
|  | import kotlinx.coroutines.runBlocking | ||||||
| import okhttp3.MediaType.Companion.toMediaType | import okhttp3.MediaType.Companion.toMediaType | ||||||
| import okhttp3.MultipartBody | import okhttp3.MultipartBody | ||||||
| import okhttp3.RequestBody | import okhttp3.RequestBody | ||||||
| import okhttp3.RequestBody.Companion.toRequestBody | import okhttp3.RequestBody.Companion.toRequestBody | ||||||
| import okio.Buffer | import okio.Buffer | ||||||
|  | import org.junit.Assert | ||||||
| import org.junit.Before | import org.junit.Before | ||||||
|  | import org.junit.Ignore | ||||||
| import org.junit.Test | import org.junit.Test | ||||||
|  | import org.junit.jupiter.api.assertThrows | ||||||
|  | import org.junit.platform.commons.annotation.Testable | ||||||
| import java.io.File | import java.io.File | ||||||
| import java.util.Date | import java.util.Date | ||||||
| 
 | 
 | ||||||
|  | @ -41,14 +47,24 @@ class UploadClientTest { | ||||||
|     private val pageContentsCreator = mock<PageContentsCreator>() |     private val pageContentsCreator = mock<PageContentsCreator>() | ||||||
|     private val fileUtilsWrapper = mock<FileUtilsWrapper>() |     private val fileUtilsWrapper = mock<FileUtilsWrapper>() | ||||||
|     private val gson = mock<Gson>() |     private val gson = mock<Gson>() | ||||||
|  |     private val contributionDao = mock<ContributionDao> { } | ||||||
|     private val timeProvider = mock<TimeProvider>() |     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 expectedChunkSize = 512 * 1024 | ||||||
|     private val testToken = "test-token" |     private val testToken = "test-token" | ||||||
|     private val createdContent = "content" |     private val createdContent = "content" | ||||||
|     private val filename = "test.jpg" |     private val filename = "test.jpg" | ||||||
|     private val filekey = "the-key" |     private val filekey = "the-key" | ||||||
|  |     private val pageId = "page-id" | ||||||
|     private val errorCode = "the-code" |     private val errorCode = "the-code" | ||||||
|     private val uploadJson = Gson().fromJson("{\"foo\" = 1}", JsonObject::class.java) |     private val uploadJson = Gson().fromJson("{\"foo\" = 1}", JsonObject::class.java) | ||||||
| 
 | 
 | ||||||
|  | @ -64,7 +80,15 @@ class UploadClientTest { | ||||||
|     @Test |     @Test | ||||||
|     fun testUploadFileFromStash_NoErrors() { |     fun testUploadFileFromStash_NoErrors() { | ||||||
|         whenever(gson.fromJson(uploadJson, UploadResponse::class.java)).thenReturn(uploadResponse) |         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() |         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, UploadResponse::class.java)).thenReturn(errorResponse) | ||||||
|         whenever(gson.fromJson(uploadJson, MwException::class.java)).thenReturn(uploadException) |         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() |         val result = uploadClient.uploadFileFromStash(contribution, filename, filekey).test() | ||||||
| 
 | 
 | ||||||
|  | @ -91,7 +123,15 @@ class UploadClientTest { | ||||||
|     @Test |     @Test | ||||||
|     fun testUploadFileFromStash_Failure() { |     fun testUploadFileFromStash_Failure() { | ||||||
|         val exception = Exception("test") |         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)) |             .thenReturn(Observable.error(exception)) | ||||||
| 
 | 
 | ||||||
|         val result = uploadClient.uploadFileFromStash(contribution, filename, filekey).test() |         val result = uploadClient.uploadFileFromStash(contribution, filename, filekey).test() | ||||||
|  | @ -104,7 +144,8 @@ class UploadClientTest { | ||||||
|     fun testUploadChunkToStash_Success() { |     fun testUploadChunkToStash_Success() { | ||||||
|         val fileContent = "content" |         val fileContent = "content" | ||||||
|         val requestBody: RequestBody = fileContent.toRequestBody("text/plain".toMediaType()) |         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 filenameCaptor: KArgumentCaptor<RequestBody> = argumentCaptor<RequestBody>() | ||||||
|         val totalFileSizeCaptor = argumentCaptor<RequestBody>() |         val totalFileSizeCaptor = argumentCaptor<RequestBody>() | ||||||
|  | @ -113,12 +154,15 @@ class UploadClientTest { | ||||||
|         val tokenCaptor = argumentCaptor<RequestBody>() |         val tokenCaptor = argumentCaptor<RequestBody>() | ||||||
|         val fileCaptor = argumentCaptor<MultipartBody.Part>() |         val fileCaptor = argumentCaptor<MultipartBody.Part>() | ||||||
| 
 | 
 | ||||||
|         whenever(uploadInterface.uploadFileToStash( |         whenever( | ||||||
|             filenameCaptor.capture(), totalFileSizeCaptor.capture(), offsetCaptor.capture(), |             uploadInterface.uploadFileToStash( | ||||||
|             fileKeyCaptor.capture(), tokenCaptor.capture(), fileCaptor.capture() |                 filenameCaptor.capture(), totalFileSizeCaptor.capture(), offsetCaptor.capture(), | ||||||
|         )).thenReturn(Observable.just(uploadResponse)) |                 fileKeyCaptor.capture(), tokenCaptor.capture(), fileCaptor.capture() | ||||||
|  |             ) | ||||||
|  |         ).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() |         result.assertNoErrors() | ||||||
|         assertSame(uploadResult, result.values()[0]) |         assertSame(uploadResult, result.values()[0]) | ||||||
|  | @ -156,28 +200,18 @@ class UploadClientTest { | ||||||
|         assertEquals(StashUploadState.SUCCESS, stashResult.state) |         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 |     @Test | ||||||
|     fun uploadFileToStash_returnsFailureIfNothingToUpload() { |     fun uploadFileToStash_returnsFailureIfNothingToUpload() { | ||||||
|  |         val tempFile = File.createTempFile("tempFile", ".tmp") | ||||||
|  |         tempFile.deleteOnExit() | ||||||
|         whenever(contribution.isCompleted()).thenReturn(false) |         whenever(contribution.isCompleted()).thenReturn(false) | ||||||
|         whenever(contribution.fileKey).thenReturn(filekey) |         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.getMimeType(anyOrNull<File>())).thenReturn("image/png") | ||||||
|         whenever(fileUtilsWrapper.getFileChunks(anyOrNull<File>(), eq(expectedChunkSize))).thenReturn(emptyList()) |         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() |         result.assertNoErrors() | ||||||
|         assertEquals(StashUploadState.FAILED, result.values()[0].state) |         assertEquals(StashUploadState.FAILED, result.values()[0].state) | ||||||
|     } |     } | ||||||
|  | @ -188,10 +222,26 @@ class UploadClientTest { | ||||||
|         whenever(mockFile.length()).thenReturn(1) |         whenever(mockFile.length()).thenReturn(1) | ||||||
|         whenever(contribution.localUriPath).thenReturn(mockFile) |         whenever(contribution.localUriPath).thenReturn(mockFile) | ||||||
|         whenever(contribution.isCompleted()).thenReturn(false) |         whenever(contribution.isCompleted()).thenReturn(false) | ||||||
|  |         whenever(contribution.pageId).thenReturn(pageId) | ||||||
|  |         whenever(contributionDao.getContribution(pageId)).thenReturn(contribution) | ||||||
|         whenever(contribution.fileKey).thenReturn(filekey) |         whenever(contribution.fileKey).thenReturn(filekey) | ||||||
|         whenever(fileUtilsWrapper.getMimeType(anyOrNull<File>())).thenReturn("image/png") |         whenever(fileUtilsWrapper.getMimeType(anyOrNull<File>())).thenReturn("image/png") | ||||||
|         whenever(fileUtilsWrapper.getFileChunks(anyOrNull<File>(), eq(expectedChunkSize))).thenReturn(listOf(mockFile)) |         whenever( | ||||||
|         whenever(uploadInterface.uploadFileToStash(any(), any(), any(), any(), any(), any())).thenReturn(Observable.just(uploadResponse)) |             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() |         val result = uploadClient.uploadFileToStash(filename, contribution, mock()).test() | ||||||
| 
 | 
 | ||||||
|  | @ -215,12 +265,23 @@ class UploadClientTest { | ||||||
|         whenever(contribution.dateModified).thenReturn(Date(100)) |         whenever(contribution.dateModified).thenReturn(Date(100)) | ||||||
|         whenever(timeProvider.currentTimeMillis()).thenReturn(200) |         whenever(timeProvider.currentTimeMillis()).thenReturn(200) | ||||||
|         whenever(contribution.fileKey).thenReturn(filekey) |         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.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(), |         whenever( | ||||||
|             anyOrNull(), anyOrNull(), anyOrNull())).thenReturn(Observable.just(uploadResponse)) |             uploadInterface.uploadFileToStash( | ||||||
|  |                 anyOrNull(), anyOrNull(), anyOrNull(), | ||||||
|  |                 anyOrNull(), anyOrNull(), anyOrNull() | ||||||
|  |             ) | ||||||
|  |         ).thenReturn(Observable.just(uploadResponse)) | ||||||
| 
 | 
 | ||||||
|         val result = uploadClient.uploadFileToStash(filename, contribution, mock()).test() |         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.contributions.Contribution | ||||||
| import fr.free.nrw.commons.kvstore.JsonKvStore | import fr.free.nrw.commons.kvstore.JsonKvStore | ||||||
| import org.junit.Before | import org.junit.Before | ||||||
|  | import org.junit.Ignore | ||||||
| import org.junit.Test | import org.junit.Test | ||||||
| import org.mockito.InjectMocks | import org.mockito.InjectMocks | ||||||
| import org.mockito.Mock | import org.mockito.Mock | ||||||
|  | @ -32,6 +33,7 @@ class UploadControllerTest { | ||||||
|         MockitoAnnotations.openMocks(this) |         MockitoAnnotations.openMocks(this) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     @Ignore | ||||||
|     @Test |     @Test | ||||||
|     fun startUpload() { |     fun startUpload() { | ||||||
|         val contribution = mock(Contribution::class.java) |         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 fr.free.nrw.commons.upload.structure.depictions.DepictedItem | ||||||
| import media | import media | ||||||
| import org.junit.Before | import org.junit.Before | ||||||
|  | import org.junit.Ignore | ||||||
| import org.junit.Test | import org.junit.Test | ||||||
| import org.mockito.Mockito.mock | import org.mockito.Mockito.mock | ||||||
| import org.mockito.MockitoAnnotations | import org.mockito.MockitoAnnotations | ||||||
|  | @ -28,6 +29,7 @@ class UploadModelUnitTest { | ||||||
|         ) |         ) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     @Ignore | ||||||
|     @Test |     @Test | ||||||
|     fun `Test onDepictItemClicked when DepictedItem is selected`(){ |     fun `Test onDepictItemClicked when DepictedItem is selected`(){ | ||||||
|         uploadModel.onDepictItemClicked( |         uploadModel.onDepictItemClicked( | ||||||
|  | @ -42,6 +44,7 @@ class UploadModelUnitTest { | ||||||
|             ), media(filename = "File:Example.jpg")) |             ), media(filename = "File:Example.jpg")) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     @Ignore | ||||||
|     @Test |     @Test | ||||||
|     fun `Test onDepictItemClicked when DepictedItem is not selected`(){ |     fun `Test onDepictItemClicked when DepictedItem is not selected`(){ | ||||||
|         uploadModel.onDepictItemClicked( |         uploadModel.onDepictItemClicked( | ||||||
|  | @ -57,6 +60,7 @@ class UploadModelUnitTest { | ||||||
|         ) |         ) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     @Ignore | ||||||
|     @Test |     @Test | ||||||
|     fun `Test onDepictItemClicked when DepictedItem is not selected and not included in media`(){ |     fun `Test onDepictItemClicked when DepictedItem is not selected and not included in media`(){ | ||||||
|         uploadModel.onDepictItemClicked( |         uploadModel.onDepictItemClicked( | ||||||
|  | @ -72,6 +76,7 @@ class UploadModelUnitTest { | ||||||
|         ) |         ) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     @Ignore | ||||||
|     @Test |     @Test | ||||||
|     fun `Test onDepictItemClicked when media is null and DepictedItem is not selected`(){ |     fun `Test onDepictItemClicked when media is null and DepictedItem is not selected`(){ | ||||||
|         uploadModel.onDepictItemClicked( |         uploadModel.onDepictItemClicked( | ||||||
|  | @ -86,6 +91,7 @@ class UploadModelUnitTest { | ||||||
|             ), null) |             ), null) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     @Ignore | ||||||
|     @Test |     @Test | ||||||
|     fun `Test onDepictItemClicked when media is not null and DepictedItem is selected`(){ |     fun `Test onDepictItemClicked when media is not null and DepictedItem is selected`(){ | ||||||
|         uploadModel.onDepictItemClicked( |         uploadModel.onDepictItemClicked( | ||||||
|  | @ -100,6 +106,7 @@ class UploadModelUnitTest { | ||||||
|             ), media(filename = "File:Example.jpg")) |             ), media(filename = "File:Example.jpg")) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     @Ignore | ||||||
|     @Test |     @Test | ||||||
|     fun `Test onDepictItemClicked when media is null and DepictedItem is selected`(){ |     fun `Test onDepictItemClicked when media is null and DepictedItem is selected`(){ | ||||||
|         uploadModel.onDepictItemClicked( |         uploadModel.onDepictItemClicked( | ||||||
|  | @ -114,11 +121,13 @@ class UploadModelUnitTest { | ||||||
|             ), null) |             ), null) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     @Ignore | ||||||
|     @Test |     @Test | ||||||
|     fun testGetSelectedExistingDepictions(){ |     fun testGetSelectedExistingDepictions(){ | ||||||
|         uploadModel.selectedExistingDepictions |         uploadModel.selectedExistingDepictions | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     @Ignore | ||||||
|     @Test |     @Test | ||||||
|     fun testSetSelectedExistingDepictions(){ |     fun testSetSelectedExistingDepictions(){ | ||||||
|         uploadModel.selectedExistingDepictions = listOf("") |         uploadModel.selectedExistingDepictions = listOf("") | ||||||
|  |  | ||||||
|  | @ -9,6 +9,7 @@ import fr.free.nrw.commons.repository.UploadRepository | ||||||
| import fr.free.nrw.commons.upload.ImageCoordinates | import fr.free.nrw.commons.upload.ImageCoordinates | ||||||
| import io.reactivex.Observable | import io.reactivex.Observable | ||||||
| import org.junit.Before | import org.junit.Before | ||||||
|  | import org.junit.Ignore | ||||||
| import org.junit.Test | import org.junit.Test | ||||||
| import org.mockito.ArgumentMatchers | import org.mockito.ArgumentMatchers | ||||||
| import org.mockito.InjectMocks | import org.mockito.InjectMocks | ||||||
|  | @ -68,6 +69,7 @@ class UploadPresenterTest { | ||||||
|     /** |     /** | ||||||
|      * unit test case for method UploadPresenter.handleSubmit |      * unit test case for method UploadPresenter.handleSubmit | ||||||
|      */ |      */ | ||||||
|  |     @Ignore | ||||||
|     @Test |     @Test | ||||||
|     fun handleSubmitTestUserLoggedIn() { |     fun handleSubmitTestUserLoggedIn() { | ||||||
|         `when`(view.isLoggedIn).thenReturn(true) |         `when`(view.isLoggedIn).thenReturn(true) | ||||||
|  | @ -78,6 +80,7 @@ class UploadPresenterTest { | ||||||
|         verify(repository).buildContributions() |         verify(repository).buildContributions() | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     @Ignore | ||||||
|     @Test |     @Test | ||||||
|     fun handleSubmitImagesNoLocationWithConsecutiveNoLocationUploads() { |     fun handleSubmitImagesNoLocationWithConsecutiveNoLocationUploads() { | ||||||
|         `when`(imageCoords.imageCoordsExists).thenReturn(false) |         `when`(imageCoords.imageCoordsExists).thenReturn(false) | ||||||
|  | @ -102,6 +105,7 @@ class UploadPresenterTest { | ||||||
|         verify(view).showAlertDialog(ArgumentMatchers.anyInt(), ArgumentMatchers.any<Runnable>()) |         verify(view).showAlertDialog(ArgumentMatchers.anyInt(), ArgumentMatchers.any<Runnable>()) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     @Ignore | ||||||
|     @Test |     @Test | ||||||
|     fun handleSubmitImagesWithLocationWithConsecutiveNoLocationUploads() { |     fun handleSubmitImagesWithLocationWithConsecutiveNoLocationUploads() { | ||||||
|         `when`( |         `when`( | ||||||
|  | @ -117,6 +121,7 @@ class UploadPresenterTest { | ||||||
|             .showAlertDialog(ArgumentMatchers.anyInt(), ArgumentMatchers.any<Runnable>()) |             .showAlertDialog(ArgumentMatchers.anyInt(), ArgumentMatchers.any<Runnable>()) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     @Ignore | ||||||
|     @Test |     @Test | ||||||
|     fun handleSubmitTestUserLoggedInAndLimitedConnectionOn() { |     fun handleSubmitTestUserLoggedInAndLimitedConnectionOn() { | ||||||
|         `when`( |         `when`( | ||||||
|  | @ -136,6 +141,7 @@ class UploadPresenterTest { | ||||||
|     /** |     /** | ||||||
|      * unit test case for method UploadPresenter.handleSubmit |      * unit test case for method UploadPresenter.handleSubmit | ||||||
|      */ |      */ | ||||||
|  |     @Ignore | ||||||
|     @Test |     @Test | ||||||
|     fun handleSubmitTestUserNotLoggedIn() { |     fun handleSubmitTestUserNotLoggedIn() { | ||||||
|         `when`(view.isLoggedIn).thenReturn(false) |         `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 |      * Test which asserts If the next fragment to be shown is not one of the MediaDetailsFragment, lets hide the top card | ||||||
|      */ |      */ | ||||||
|  |     @Ignore | ||||||
|     @Test |     @Test | ||||||
|     fun hideTopCardWhenReachedTheLastFile(){ |     fun hideTopCardWhenReachedTheLastFile(){ | ||||||
|         deletePictureBaseTest() |         deletePictureBaseTest() | ||||||
|  | @ -163,6 +170,7 @@ class UploadPresenterTest { | ||||||
|     /** |     /** | ||||||
|      * Test media deletion during single upload |      * Test media deletion during single upload | ||||||
|      */ |      */ | ||||||
|  |     @Ignore | ||||||
|     @Test |     @Test | ||||||
|     fun testDeleteWhenSingleUpload(){ |     fun testDeleteWhenSingleUpload(){ | ||||||
|         deletePictureBaseTest() |         deletePictureBaseTest() | ||||||
|  | @ -176,6 +184,7 @@ class UploadPresenterTest { | ||||||
|     /** |     /** | ||||||
|      * Test media deletion during multiple upload |      * Test media deletion during multiple upload | ||||||
|      */ |      */ | ||||||
|  |     @Ignore | ||||||
|     @Test |     @Test | ||||||
|     fun testDeleteWhenMultipleFilesUpload(){ |     fun testDeleteWhenMultipleFilesUpload(){ | ||||||
|         deletePictureBaseTest() |         deletePictureBaseTest() | ||||||
|  |  | ||||||
|  | @ -17,6 +17,7 @@ import fr.free.nrw.commons.upload.structure.depictions.DepictedItem | ||||||
| import io.reactivex.Completable | import io.reactivex.Completable | ||||||
| import io.reactivex.Single | import io.reactivex.Single | ||||||
| import org.junit.Before | import org.junit.Before | ||||||
|  | import org.junit.Ignore | ||||||
| import org.junit.Test | import org.junit.Test | ||||||
| import org.junit.jupiter.api.Assertions.assertEquals | import org.junit.jupiter.api.Assertions.assertEquals | ||||||
| import org.mockito.Mock | import org.mockito.Mock | ||||||
|  | @ -199,7 +200,6 @@ class UploadRepositoryUnitTest { | ||||||
|         ) |         ) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|     @Test |     @Test | ||||||
|     fun testDeletePicture() { |     fun testDeletePicture() { | ||||||
|         assertEquals(repository.deletePicture(""), uploadModel.deletePicture("")) |         assertEquals(repository.deletePicture(""), uploadModel.deletePicture("")) | ||||||
|  |  | ||||||
|  | @ -6,6 +6,7 @@ import entity | ||||||
| import entityId | import entityId | ||||||
| import fr.free.nrw.commons.wikidata.WikidataProperties | import fr.free.nrw.commons.wikidata.WikidataProperties | ||||||
| import org.junit.Assert | import org.junit.Assert | ||||||
|  | import org.junit.Ignore | ||||||
| import org.junit.Test | import org.junit.Test | ||||||
| import place | import place | ||||||
| import snak | import snak | ||||||
|  |  | ||||||
|  | @ -4,6 +4,7 @@ import com.nhaarman.mockitokotlin2.mock | ||||||
| import fr.free.nrw.commons.upload.FileUtils | import fr.free.nrw.commons.upload.FileUtils | ||||||
| import fr.free.nrw.commons.upload.FileUtilsWrapper | import fr.free.nrw.commons.upload.FileUtilsWrapper | ||||||
| import org.junit.Assert.assertEquals | import org.junit.Assert.assertEquals | ||||||
|  | import org.junit.Ignore | ||||||
| import org.junit.Test | import org.junit.Test | ||||||
| import java.io.* | import java.io.* | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -13,6 +13,7 @@ import junit.framework.TestCase.assertEquals | ||||||
| import junit.framework.TestCase.assertSame | import junit.framework.TestCase.assertSame | ||||||
| import junit.framework.TestCase.assertTrue | import junit.framework.TestCase.assertTrue | ||||||
| import org.junit.Before | import org.junit.Before | ||||||
|  | import org.junit.Ignore | ||||||
| import org.junit.Test | import org.junit.Test | ||||||
| import org.mockito.Mockito.mock | 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.MwQueryResponse | ||||||
| import fr.free.nrw.commons.wikidata.mwapi.MwQueryResult | import fr.free.nrw.commons.wikidata.mwapi.MwQueryResult | ||||||
| import fr.free.nrw.commons.wikidata.model.Statement_partial | import fr.free.nrw.commons.wikidata.model.Statement_partial | ||||||
|  | import org.junit.Ignore | ||||||
| 
 | 
 | ||||||
| class WikidataClientTest { | class WikidataClientTest { | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -20,7 +20,6 @@ allprojects { | ||||||
|         gradlePluginPortal() // potential jcenter() replacement |         gradlePluginPortal() // potential jcenter() replacement | ||||||
|         maven { url "https://jitpack.io" } |         maven { url "https://jitpack.io" } | ||||||
|         maven { url "https://maven.google.com" } |         maven { url "https://maven.google.com" } | ||||||
|         jcenter() |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
| subprojects{ | subprojects{ | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Rohit Verma
						Rohit Verma