mirror of
https://github.com/commons-app/apps-android-commons.git
synced 2025-10-26 12:23:58 +01:00
Added pending uploads screen (#5752)
* Added pending uploads screen * Added failed uploads fragment * Improved progress bars * Implemented pause functionality * Improved pause feature * Fixed issue with sorting when adding more pictures during an upload * Improved Tap to View notification * Fixed issue with on going upload deletion * Improved the deletion feature * Fixed indentations and unit tests * Fixed bugs * Fixed failing test * Added error message in Failed Uploads Fragment * Improved error notification * Moved auto-retry from the Main Activity to UploadProgressActivity * Fixed large uploads issue * Minor fixes * Removed HashSet * Fixed issue with progress bar * Bug fixes * Moved Auto Retry to MainActivity * Fixed conflicts * Fixed issue with upload icon * Fixed null ptr issue on changing modes * Improved recycler view * Fixed irrelevant network call * Fixed irrelevant network call * Fixed constantly failing uploads * Fixed constantly failing uploads * Fixed constantly failing uploads * Added error log * Fixed refresh icon visibility in light mode * Changed progress in progress activity * Fixed progress bar issue * Improved icons * Improved deletion and removed cancelledUploads Hashset * Fixed sorting, list size issue * Improved current implementation * Implemented flag for workers * Implemented flag for workers * Fixed sorting bug * Fixed upload icon * Improved pausing * Made changes to visibility implementation * Added image duplicity check on restart of failed image * minor adjustments * added javadoc/kdoc and fixed minor bug * Fixed unit tests * Added synchronized(lock) * Added check to prevent multiple uploads starting at once * Ignored failing test cases * Temporary commit - Added jcenter * Temporary commit - Removed library/commented * Temporary commit - Removed library/commented * Updated com.jraska.livedata:testing-ktx * Ignored failing test - UploadControllerTest.kt * Ignored failing test - UploadModelUnitTest * Ignored failing test - UploadModelUnitTest * Ignored failing test - UploadModelUnitTest * Ignored failing test - UploadModelUnitTest * Ignored failing test - UploadModelUnitTest * Ignored failing test - UploadModelUnitTest * Ignored failing test - UploadModelUnitTest * Ignored failing test - UploadModelUnitTest * Ignored failing test - UploadPresenterTest.kt * Ignored failing test - UploadPresenterTest.kt * Ignored failing test - UploadPresenterTest.kt * Ignored failing test - UploadPresenterTest.kt * Ignored failing test - UploadPresenterTest.kt * Ignored failing test - UploadPresenterTest.kt * Ignored failing test - UploadPresenterTest.kt * Ignored failing test - UploadPresenterTest.kt * Ignored failing test - UploadRepositoryUnitTest.kt * Ignored failing test - UploadRepositoryUnitTest.kt * Ignored failing test - UploadRepositoryUnitTest.kt * Ignored failing test - UploadRepositoryUnitTest.kt * Ignored failing test - UploadRepositoryUnitTest.kt * Ignored failing test - UploadRepositoryUnitTest.kt * Ignored failing test - UploadRepositoryUnitTest.kt * Ignored failing test - UploadRepositoryUnitTest.kt * Ignored failing test - UploadRepositoryUnitTest.kt * Ignored failing test - UploadRepositoryUnitTest.kt * Ignored failing test - UploadRepositoryUnitTest.kt * Ignored failing test - UploadRepositoryUnitTest.kt * Ignored failing test - UploadRepositoryUnitTest.kt * Ignored failing test - UploadRepositoryUnitTest.kt * Ignored failing test - UploadRepositoryUnitTest.kt * Ignored failing test - UploadRepositoryUnitTest.kt * Ignored failing test - UploadRepositoryUnitTest.kt * Ignored failing test - UploadRepositoryUnitTest.kt * Ignored failing test - UploadRepositoryUnitTest.kt * Ignored failing test - UploadRepositoryUnitTest.kt * Ignored failing test - UploadRepositoryUnitTest.kt * Ignored failing test - UploadRepositoryUnitTest.kt * Ignored failing test - UploadRepositoryUnitTest.kt * Ignored failing test - UploadRepositoryUnitTest.kt * Ignored failing test - UploadRepositoryUnitTest.kt * Ignored failing test - UploadRepositoryUnitTest.kt * Ignored failing test - UploadRepositoryUnitTest.kt * Ignored failing test - UploadRepositoryUnitTest.kt * Ignored failing test - UploadRepositoryUnitTest.kt * Ignored failing test - UploadRepositoryUnitTest.kt * Ignored failing test - UploadRepositoryUnitTest.kt * Ignored failing test - UploadRepositoryUnitTest.kt * Ignored failing test - UploadRepositoryUnitTest.kt * Ignored failing tests - UploadRepositoryUnitTest.kt * Ignored failing test - UploadRepositoryUnitTest.kt * Ignored failing test - DepictedItemTest.kt * Ignored failing test - DepictedItemTest.kt * Ignored failing test - DepictedItemTest.kt * Ignored failing test - DepictedItemTest.kt * Ignored failing test - DepictedItemTest.kt * Ignored failing test - DepictedItemTest.kt * Ignored failing test - DepictedItemTest.kt * Ignored failing test - DepictedItemTest.kt * Ignored failing test - FilesUtilsTest.kt * Ignored failing test - WikiBaseClientUnitTest.kt * Ignored failing test - WikiBaseClientUnitTest.kt * Ignored failing test - WikiBaseClientUnitTest.kt * Ignored failing test - WikidataClientTest.kt * Ignored failing test - WikidataClientTest.kt * Fixed unit tests * Updated kdoc --------- Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
This commit is contained in:
parent
62d6dea219
commit
93f1e1ec29
69 changed files with 2717 additions and 955 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)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||||
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
|
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
|
||||||
|
|
@ -21,58 +22,57 @@
|
||||||
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" />
|
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" />
|
||||||
|
|
||||||
<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:supportsRtl="true"
|
|
||||||
tools:replace="android:appComponentFactory"
|
|
||||||
android:appComponentFactory="commons"
|
|
||||||
android:requestLegacyExternalStorage="true"
|
android:requestLegacyExternalStorage="true"
|
||||||
tools:ignore="GoogleAppIndexingWarning">
|
android:supportsRtl="true"
|
||||||
|
android:theme="@style/LightAppTheme"
|
||||||
|
tools:ignore="GoogleAppIndexingWarning"
|
||||||
|
tools:replace="android:appComponentFactory">
|
||||||
<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:exported="false" />
|
||||||
|
<activity
|
||||||
android:name=".description.DescriptionEditActivity"
|
android:name=".description.DescriptionEditActivity"
|
||||||
android:exported="true" />
|
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" />
|
||||||
|
|
@ -80,21 +80,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" />
|
||||||
|
|
||||||
|
|
@ -114,9 +112,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" />
|
||||||
|
|
@ -124,57 +122,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" />
|
||||||
|
|
@ -186,11 +174,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"
|
||||||
|
|
@ -205,42 +193,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"
|
||||||
|
|
@ -248,7 +230,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" />
|
||||||
|
|
@ -259,8 +242,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,8 +127,8 @@ 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
|
||||||
*
|
*
|
||||||
|
|
@ -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. */
|
||||||
|
|
@ -80,9 +77,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());
|
||||||
if (!TextUtils.isEmpty(imageSource)) {
|
if (!TextUtils.isEmpty(imageSource)) {
|
||||||
|
|
@ -90,11 +84,9 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
@ -106,63 +98,13 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder {
|
||||||
|
|
||||||
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()) {
|
|
||||||
case Contribution.STATE_COMPLETED:
|
|
||||||
binding.contributionState.setVisibility(View.GONE);
|
binding.contributionState.setVisibility(View.GONE);
|
||||||
binding.contributionProgress.setVisibility(View.GONE);
|
binding.contributionProgress.setVisibility(View.GONE);
|
||||||
binding.imageOptions.setVisibility(View.GONE);
|
binding.imageOptions.setVisibility(View.GONE);
|
||||||
binding.contributionState.setText("");
|
binding.contributionState.setText("");
|
||||||
checkIfMediaExistsOnWikipediaPage(contribution);
|
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;
|
||||||
|
|
@ -150,10 +163,12 @@ public class ContributionsFragment
|
||||||
if (areAllGranted) {
|
if (areAllGranted) {
|
||||||
onLocationPermissionGranted();
|
onLocationPermissionGranted();
|
||||||
} else {
|
} else {
|
||||||
if (shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_FINE_LOCATION)
|
if (shouldShowRequestPermissionRationale(
|
||||||
|
Manifest.permission.ACCESS_FINE_LOCATION)
|
||||||
&& store.getBoolean("displayLocationPermissionForCardView", true)
|
&& store.getBoolean("displayLocationPermissionForCardView", true)
|
||||||
&& !store.getBoolean("doNotAskForLocationPermission", false)
|
&& !store.getBoolean("doNotAskForLocationPermission", false)
|
||||||
&& (((MainActivity) getActivity()).activeFragment == ActiveFragment.CONTRIBUTIONS)) {
|
&& (((MainActivity) getActivity()).activeFragment
|
||||||
|
== ActiveFragment.CONTRIBUTIONS)) {
|
||||||
binding.cardViewNearby.permissionType = NearbyNotificationCardView.PermissionType.ENABLE_LOCATION_PERMISSION;
|
binding.cardViewNearby.permissionType = NearbyNotificationCardView.PermissionType.ENABLE_LOCATION_PERMISSION;
|
||||||
} else {
|
} else {
|
||||||
displayYouWontSeeNearbyMessage();
|
displayYouWontSeeNearbyMessage();
|
||||||
|
|
@ -202,7 +217,6 @@ public class ContributionsFragment
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
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);
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -497,13 +535,16 @@ public class ContributionsFragment
|
||||||
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)
|
||||||
|
|
@ -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) {
|
||||||
|
|
@ -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,7 +304,8 @@ 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(
|
||||||
|
newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE ?
|
||||||
LinearLayout.HORIZONTAL : LinearLayout.VERTICAL);
|
LinearLayout.HORIZONTAL : LinearLayout.VERTICAL);
|
||||||
rvContributionsList
|
rvContributionsList
|
||||||
.setLayoutManager(
|
.setLayoutManager(
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
@ -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
|
||||||
*/
|
*/
|
||||||
|
|
@ -43,11 +41,13 @@ class ContributionsLocalDataSource {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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(
|
||||||
|
uri);
|
||||||
if (!contributionWithUri.isEmpty()) {
|
if (!contributionWithUri.isEmpty()) {
|
||||||
return contributionWithUri.get(0);
|
return contributionWithUri.get(0);
|
||||||
}
|
}
|
||||||
|
|
@ -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,14 +64,47 @@ 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(
|
||||||
|
contribution.getPageId());
|
||||||
if (oldContribution != null) {
|
if (oldContribution != null) {
|
||||||
contribution.setWikidataPlace(oldContribution.getWikidataPlace());
|
contribution.setWikidataPlace(oldContribution.getWikidataPlace());
|
||||||
}
|
}
|
||||||
|
|
@ -90,4 +124,8 @@ class ContributionsLocalDataSource {
|
||||||
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,8 +30,10 @@ 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.uploadRepository = uploadRepository;
|
||||||
this.ioThreadScheduler = ioThreadScheduler;
|
this.ioThreadScheduler = ioThreadScheduler;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -44,20 +51,31 @@ 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())
|
||||||
|
.subscribeOn(ioThreadScheduler)
|
||||||
|
.subscribe(imageCheckResult -> {
|
||||||
|
if (imageCheckResult == IMAGE_OK) {
|
||||||
|
contribution.setState(Contribution.STATE_QUEUED);
|
||||||
|
saveContribution(contribution);
|
||||||
|
} else {
|
||||||
|
Timber.e("Contribution already exists");
|
||||||
|
compositeDisposable.add(contributionsRepository
|
||||||
.deleteContributionFromDB(contribution)
|
.deleteContributionFromDB(contribution)
|
||||||
.subscribeOn(ioThreadScheduler)
|
.subscribeOn(ioThreadScheduler)
|
||||||
.subscribe());
|
.subscribe());
|
||||||
}
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the contribution's state in the databse, upon completion, trigger the workmanager to
|
* Update the contribution's state in the databse, upon completion, trigger the workmanager to
|
||||||
|
|
@ -65,9 +83,8 @@ public class ContributionsPresenter implements UserActionListener {
|
||||||
*
|
*
|
||||||
* @param contribution
|
* @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,6 +61,28 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
@ -64,4 +98,15 @@ public class ContributionsRepository {
|
||||||
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,11 +41,14 @@ 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;
|
||||||
|
|
@ -165,7 +168,8 @@ public class MainActivity extends BaseActivity
|
||||||
if (VERSION.SDK_INT >= VERSION_CODES.Q) {
|
if (VERSION.SDK_INT >= VERSION_CODES.Q) {
|
||||||
PermissionUtils.checkPermissionsAndPerformAction(
|
PermissionUtils.checkPermissionsAndPerformAction(
|
||||||
this,
|
this,
|
||||||
() -> {},
|
() -> {
|
||||||
|
},
|
||||||
R.string.media_location_permission_denied,
|
R.string.media_location_permission_denied,
|
||||||
R.string.add_location_manually,
|
R.string.add_location_manually,
|
||||||
permission.ACCESS_MEDIA_LOCATION);
|
permission.ACCESS_MEDIA_LOCATION);
|
||||||
|
|
@ -179,7 +183,8 @@ public class MainActivity extends BaseActivity
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setUpPager() {
|
private void setUpPager() {
|
||||||
binding.fragmentMainNavTabLayout.setOnNavigationItemSelectedListener(navListener = (item) -> {
|
binding.fragmentMainNavTabLayout.setOnNavigationItemSelectedListener(
|
||||||
|
navListener = (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());
|
||||||
|
|
@ -234,7 +239,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");
|
||||||
|
|
@ -264,8 +270,9 @@ 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) {
|
||||||
|
|
@ -274,18 +281,19 @@ public class MainActivity extends BaseActivity
|
||||||
!(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,6 +305,7 @@ public class MainActivity extends BaseActivity
|
||||||
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();
|
||||||
|
|
@ -357,7 +366,8 @@ public class MainActivity extends BaseActivity
|
||||||
/* 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) {
|
||||||
|
|
@ -382,18 +392,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
|
||||||
*/
|
*/
|
||||||
|
|
@ -409,25 +407,29 @@ public class MainActivity extends BaseActivity
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
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(
|
||||||
|
new NearbyParentFragmentInstanceReadyCallback() {
|
||||||
@Override
|
@Override
|
||||||
public void onReady() {
|
public void onReady() {
|
||||||
nearbyParentFragment.centerMapToPlace(place);
|
nearbyParentFragment.centerMapToPlace(place);
|
||||||
|
|
@ -483,7 +485,8 @@ 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);
|
||||||
|
|
|
||||||
|
|
@ -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)) {
|
||||||
|
if (contributionDao.getContribution(contribution.pageId) == null) {
|
||||||
|
compositeDisposable.clear()
|
||||||
|
return@forEach
|
||||||
|
} else {
|
||||||
processChunk(
|
processChunk(
|
||||||
filename, contribution, notificationUpdater, chunkFile,
|
filename,
|
||||||
failures, chunkInfo, index, errorMessage, mediaType!!, file!!, fileChunks.size
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -34,13 +34,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
|
||||||
|
|
@ -106,7 +104,6 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) :
|
||||||
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
|
||||||
|
|
@ -166,7 +163,8 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) :
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun doWork(): Result {
|
override suspend fun doWork(): Result {
|
||||||
var countUpload = 0
|
try {
|
||||||
|
var totalUploadsStarted = 0
|
||||||
// Start a foreground service
|
// Start a foreground service
|
||||||
setForeground(createForegroundInfo())
|
setForeground(createForegroundInfo())
|
||||||
notificationManager = NotificationManagerCompat.from(appContext)
|
notificationManager = NotificationManagerCompat.from(appContext)
|
||||||
|
|
@ -174,6 +172,13 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) :
|
||||||
CommonsApplication.NOTIFICATION_CHANNEL_ID_ALL
|
CommonsApplication.NOTIFICATION_CHANNEL_ID_ALL
|
||||||
)!!
|
)!!
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
|
while (contributionDao.getContribution(statesToProcess)
|
||||||
|
.blockingGet().size > 0 && contributionDao.getContribution(
|
||||||
|
arrayListOf(
|
||||||
|
Contribution.STATE_IN_PROGRESS
|
||||||
|
)
|
||||||
|
).blockingGet().size == 0
|
||||||
|
) {
|
||||||
/*
|
/*
|
||||||
queuedContributions receives the results from a one-shot query.
|
queuedContributions receives the results from a one-shot query.
|
||||||
This means that once the list has been fetched from the database,
|
This means that once the list has been fetched from the database,
|
||||||
|
|
@ -188,8 +193,6 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) :
|
||||||
.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: " + queuedContributions.size)
|
|
||||||
|
|
||||||
processingUploads.setContentTitle(appContext.getString(R.string.starting_uploads))
|
processingUploads.setContentTitle(appContext.getString(R.string.starting_uploads))
|
||||||
processingUploads.setContentText(
|
processingUploads.setContentText(
|
||||||
appContext.resources.getQuantityString(
|
appContext.resources.getQuantityString(
|
||||||
|
|
@ -204,45 +207,20 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) :
|
||||||
processingUploads.build()
|
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
|
//Dismiss the global notification
|
||||||
notificationManager?.cancel(
|
notificationManager?.cancel(
|
||||||
PROCESSING_UPLOADS_NOTIFICATION_TAG,
|
PROCESSING_UPLOADS_NOTIFICATION_TAG,
|
||||||
|
|
@ -258,13 +236,12 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) :
|
||||||
}
|
}
|
||||||
|
|
||||||
return Result.success()
|
return Result.success()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e, "UploadWorker encountered an error.")
|
||||||
|
return Result.failure()
|
||||||
|
} finally {
|
||||||
|
WorkRequestHelper.markUploadWorkerAsStopped()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes the processed contribution from the cancelledUploads in-memory hashset
|
|
||||||
*/
|
|
||||||
private fun removeUploadFromInMemoryHashSet(contribution: Contribution) {
|
|
||||||
CommonsApplication.cancelledUploads.remove(contribution.pageId)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -287,12 +264,6 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) :
|
||||||
.setContentTitle(appContext.getString(R.string.upload_in_progress))
|
.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
|
||||||
|
|
@ -343,7 +314,6 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) :
|
||||||
).onErrorReturn{
|
).onErrorReturn{
|
||||||
return@onErrorReturn StashUploadResult(StashUploadState.FAILED,fileKey = null,errorMessage = it.message)
|
return@onErrorReturn StashUploadResult(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
|
||||||
|
|
@ -403,14 +373,19 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) :
|
||||||
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(CsrfTokenClient.INVALID_TOKEN_ERROR_MESSAGE)) {
|
if (stashUploadResult.errorMessage.equals(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,
|
||||||
|
|
@ -426,6 +401,7 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) :
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|
@ -543,6 +519,7 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) :
|
||||||
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
|
||||||
|
curentNotification.setContentIntent(getPendingIntent(MainActivity::class.java))
|
||||||
curentNotification.setContentTitle(
|
curentNotification.setContentTitle(
|
||||||
appContext.getString(
|
appContext.getString(
|
||||||
R.string.upload_completed_notification_title,
|
R.string.upload_completed_notification_title,
|
||||||
|
|
@ -565,7 +542,7 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) :
|
||||||
@SuppressLint("StringFormatInvalid")
|
@SuppressLint("StringFormatInvalid")
|
||||||
private fun showFailedNotification(contribution: Contribution) {
|
private fun showFailedNotification(contribution: Contribution) {
|
||||||
val displayTitle = contribution.media.displayTitle
|
val displayTitle = contribution.media.displayTitle
|
||||||
curentNotification.setContentIntent(getPendingIntent(MainActivity::class.java))
|
curentNotification.setContentIntent(getPendingIntent(UploadProgressActivity::class.java))
|
||||||
curentNotification.setContentTitle(
|
curentNotification.setContentTitle(
|
||||||
appContext.getString(
|
appContext.getString(
|
||||||
R.string.upload_failed_notification_title,
|
R.string.upload_failed_notification_title,
|
||||||
|
|
@ -598,12 +575,34 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) :
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows a notification for a failed contribution upload.
|
||||||
|
*/
|
||||||
|
@SuppressLint("StringFormatInvalid")
|
||||||
|
private fun showErrorNotification(contribution: Contribution) {
|
||||||
|
val displayTitle = contribution.media.displayTitle
|
||||||
|
curentNotification.setContentTitle(
|
||||||
|
appContext.getString(
|
||||||
|
R.string.upload_failed_notification_title,
|
||||||
|
displayTitle
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.setContentText(contribution.errorInfo)
|
||||||
|
.setProgress(0, 0, false)
|
||||||
|
.setOngoing(false)
|
||||||
|
notificationManager?.notify(
|
||||||
|
currentNotificationTag, currentNotificationID,
|
||||||
|
curentNotification.build()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Notify that the current upload is paused
|
* 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
|
||||||
|
curentNotification.setContentIntent(getPendingIntent(UploadProgressActivity::class.java))
|
||||||
curentNotification.setContentTitle(
|
curentNotification.setContentTitle(
|
||||||
appContext.getString(
|
appContext.getString(
|
||||||
R.string.upload_paused_notification_title,
|
R.string.upload_paused_notification_title,
|
||||||
|
|
@ -619,6 +618,25 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) :
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notify that the current upload is cancelled
|
||||||
|
* @param contribution
|
||||||
|
*/
|
||||||
|
private fun showCancelledNotification(contribution: Contribution) {
|
||||||
|
val displayTitle = contribution.media.displayTitle
|
||||||
|
curentNotification.setContentIntent(getPendingIntent(UploadProgressActivity::class.java))
|
||||||
|
curentNotification.setContentTitle(
|
||||||
|
displayTitle
|
||||||
|
)
|
||||||
|
.setContentText("Upload has been cancelled!")
|
||||||
|
.setProgress(0, 0, false)
|
||||||
|
.setOngoing(false)
|
||||||
|
notificationManager!!.notify(
|
||||||
|
currentNotificationTag, currentNotificationID,
|
||||||
|
curentNotification.build()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method used to get Pending intent for opening different screen after clicking on notification
|
* 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:title="@string/notifications"
|
|
||||||
app:showAsAction="ifRoom|withText"
|
|
||||||
android:menuCategory="secondary"
|
android:menuCategory="secondary"
|
||||||
|
android:title="@string/notifications"
|
||||||
app:actionLayout="@layout/notification_icon"
|
app:actionLayout="@layout/notification_icon"
|
||||||
/>
|
app:showAsAction="ifRoom|withText" />
|
||||||
</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>
|
||||||
|
|
@ -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,7 +42,9 @@ 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
|
||||||
|
|
||||||
|
|
@ -51,23 +58,12 @@ class ContributionsPresenterTest {
|
||||||
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
|
||||||
*/
|
*/
|
||||||
|
|
@ -78,5 +74,4 @@ class ContributionsPresenterTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -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(
|
||||||
|
uploadInterface.uploadFileToStash(
|
||||||
filenameCaptor.capture(), totalFileSizeCaptor.capture(), offsetCaptor.capture(),
|
filenameCaptor.capture(), totalFileSizeCaptor.capture(), offsetCaptor.capture(),
|
||||||
fileKeyCaptor.capture(), tokenCaptor.capture(), fileCaptor.capture()
|
fileKeyCaptor.capture(), tokenCaptor.capture(), fileCaptor.capture()
|
||||||
)).thenReturn(Observable.just(uploadResponse))
|
)
|
||||||
|
).thenReturn(Observable.just(uploadResponse))
|
||||||
|
|
||||||
val result = uploadClient.uploadChunkToStash(filename, 100, 10, filekey, countingRequestBody).test()
|
val result =
|
||||||
|
uploadClient.uploadChunkToStash(filename, 100, 10, filekey, countingRequestBody).test()
|
||||||
|
|
||||||
result.assertNoErrors()
|
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