resultLauncher) {
- DialogUtil.showAlertDialog(activity, activity.getString(R.string.location_permission_title),
- activity.getString(R.string.in_app_camera_location_permission_rationale),
- activity.getString(android.R.string.ok),
- activity.getString(android.R.string.cancel),
- () -> {
- createDialogsAndHandleLocationPermissions(activity,
- inAppCameraLocationPermissionLauncher, resultLauncher);
- },
- () -> locationPermissionCallback.onLocationPermissionDenied(
- activity.getString(R.string.in_app_camera_location_permission_denied)),
- null,
- false);
- }
-
- /**
- * Suggest user to attach location information with pictures. If the user selects "Yes", then:
- *
- * Location is taken from the EXIF if the default camera application does not redact location
- * tags.
- *
- * Otherwise, if the EXIF metadata does not have location information, then location captured by
- * the app is used
- *
- * @param activity
- */
- private void askUserToAllowLocationAccess(Activity activity,
- ActivityResultLauncher inAppCameraLocationPermissionLauncher,
- ActivityResultLauncher resultLauncher) {
- DialogUtil.showAlertDialog(activity,
- activity.getString(R.string.in_app_camera_location_permission_title),
- activity.getString(R.string.in_app_camera_location_access_explanation),
- activity.getString(R.string.option_allow),
- activity.getString(R.string.option_dismiss),
- () -> {
- defaultKvStore.putBoolean("inAppCameraLocationPref", true);
- createDialogsAndHandleLocationPermissions(activity,
- inAppCameraLocationPermissionLauncher, resultLauncher);
- },
- () -> {
- ViewUtil.showLongToast(activity, R.string.in_app_camera_location_permission_denied);
- defaultKvStore.putBoolean("inAppCameraLocationPref", false);
- initiateCameraUpload(activity, resultLauncher);
- },
- null,
- true);
- }
-
- /**
- * Initiate gallery picker
- */
- public void initiateGalleryPick(final Activity activity, ActivityResultLauncher resultLauncher, final boolean allowMultipleUploads) {
- initiateGalleryUpload(activity, resultLauncher, allowMultipleUploads);
- }
-
- /**
- * Initiate gallery picker with permission
- */
- public void initiateCustomGalleryPickWithPermission(final Activity activity, ActivityResultLauncher resultLauncher) {
- setPickerConfiguration(activity, true);
-
- PermissionUtils.checkPermissionsAndPerformAction(activity,
- () -> FilePicker.openCustomSelector(activity, resultLauncher, 0),
- R.string.storage_permission_title,
- R.string.write_storage_permission_rationale,
- PermissionUtils.PERMISSIONS_STORAGE);
- }
-
-
- /**
- * Open chooser for gallery uploads
- */
- private void initiateGalleryUpload(final Activity activity, ActivityResultLauncher resultLauncher,
- final boolean allowMultipleUploads) {
- setPickerConfiguration(activity, allowMultipleUploads);
- FilePicker.openGallery(activity, resultLauncher, 0, isDocumentPhotoPickerPreferred());
- }
-
- /**
- * Sets configuration for file picker
- */
- private void setPickerConfiguration(Activity activity,
- boolean allowMultipleUploads) {
- boolean copyToExternalStorage = defaultKvStore.getBoolean("useExternalStorage", true);
- FilePicker.configuration(activity)
- .setCopyTakenPhotosToPublicGalleryAppFolder(copyToExternalStorage)
- .setAllowMultiplePickInGallery(allowMultipleUploads);
- }
-
- /**
- * Initiate camera upload by opening camera
- */
- private void initiateCameraUpload(Activity activity, ActivityResultLauncher resultLauncher) {
- setPickerConfiguration(activity, false);
- if (defaultKvStore.getBoolean("inAppCameraLocationPref", false)) {
- locationBeforeImageCapture = locationManager.getLastLocation();
- }
- isInAppCameraUpload = true;
- FilePicker.openCameraForImage(activity, resultLauncher, 0);
- }
-
- private boolean isDocumentPhotoPickerPreferred(){
- return defaultKvStore.getBoolean(
- "openDocumentPhotoPickerPref", true);
- }
-
- public void onPictureReturnedFromGallery(ActivityResult result, Activity activity, FilePicker.Callbacks callbacks){
-
- if(isDocumentPhotoPickerPreferred()){
- FilePicker.onPictureReturnedFromDocuments(result, activity, callbacks);
- } else {
- FilePicker.onPictureReturnedFromGallery(result, activity, callbacks);
- }
- }
-
- public void onPictureReturnedFromCustomSelector(ActivityResult result, Activity activity, @NonNull FilePicker.Callbacks callbacks) {
- FilePicker.onPictureReturnedFromCustomSelector(result, activity, callbacks);
- }
-
- public void onPictureReturnedFromCamera(ActivityResult result, Activity activity, @NonNull FilePicker.Callbacks callbacks) {
- FilePicker.onPictureReturnedFromCamera(result, activity, callbacks);
- }
-
- /**
- * Attaches callback for file picker.
- */
- public void handleActivityResultWithCallback(Activity activity, FilePicker.HandleActivityResult handleActivityResult) {
-
- handleActivityResult.onHandleActivityResult(new DefaultCallback() {
-
- @Override
- public void onCanceled(final ImageSource source, final int type) {
- super.onCanceled(source, type);
- defaultKvStore.remove(PLACE_OBJECT);
- }
-
- @Override
- public void onImagePickerError(Exception e, FilePicker.ImageSource source,
- int type) {
- ViewUtil.showShortToast(activity, R.string.error_occurred_in_picking_images);
- }
-
- @Override
- public void onImagesPicked(@NonNull List imagesFiles,
- FilePicker.ImageSource source, int type) {
- Intent intent = handleImagesPicked(activity, imagesFiles);
- activity.startActivity(intent);
- }
- });
- }
-
- public List handleExternalImagesPicked(Activity activity,
- Intent data) {
- return FilePicker.handleExternalImagesPicked(data, activity);
- }
-
- /**
- * Returns intent to be passed to upload activity Attaches place object for nearby uploads and
- * location before image capture if in-app camera is used
- */
- private Intent handleImagesPicked(Context context,
- List imagesFiles) {
- Intent shareIntent = new Intent(context, UploadActivity.class);
- shareIntent.setAction(ACTION_INTERNAL_UPLOADS);
- shareIntent
- .putParcelableArrayListExtra(UploadActivity.EXTRA_FILES, new ArrayList<>(imagesFiles));
- Place place = defaultKvStore.getJson(PLACE_OBJECT, Place.class);
-
- if (place != null) {
- shareIntent.putExtra(PLACE_OBJECT, place);
- }
-
- if (locationBeforeImageCapture != null) {
- shareIntent.putExtra(
- UploadActivity.LOCATION_BEFORE_IMAGE_CAPTURE,
- locationBeforeImageCapture);
- }
-
- shareIntent.putExtra(
- UploadActivity.IN_APP_CAMERA_UPLOAD,
- isInAppCameraUpload
- );
- isInAppCameraUpload = false; // reset the flag for next use
- return shareIntent;
- }
-
- /**
- * Fetches the contributions with the state "IN_PROGRESS", "QUEUED" and "PAUSED" and then it
- * populates the `pendingContributionList`.
- **/
- void getPendingContributions() {
- final PagedList.Config pagedListConfig =
- (new PagedList.Config.Builder())
- .setPrefetchDistance(50)
- .setPageSize(10).build();
- Factory 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 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 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();
- }
-}
diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.kt b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.kt
new file mode 100644
index 0000000000..296391c6df
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.kt
@@ -0,0 +1,474 @@
+package fr.free.nrw.commons.contributions
+
+import android.Manifest.permission
+import android.app.Activity
+import android.content.Context
+import android.content.Intent
+import android.widget.Toast
+import androidx.activity.result.ActivityResult
+import androidx.activity.result.ActivityResultLauncher
+import androidx.lifecycle.LiveData
+import androidx.paging.LivePagedListBuilder
+import androidx.paging.PagedList
+import fr.free.nrw.commons.R
+import fr.free.nrw.commons.filepicker.DefaultCallback
+import fr.free.nrw.commons.filepicker.FilePicker
+import fr.free.nrw.commons.filepicker.FilePicker.HandleActivityResult
+import fr.free.nrw.commons.filepicker.FilePicker.configuration
+import fr.free.nrw.commons.filepicker.FilePicker.handleExternalImagesPicked
+import fr.free.nrw.commons.filepicker.FilePicker.onPictureReturnedFromDocuments
+import fr.free.nrw.commons.filepicker.FilePicker.openCameraForImage
+import fr.free.nrw.commons.filepicker.FilePicker.openCustomSelector
+import fr.free.nrw.commons.filepicker.FilePicker.openGallery
+import fr.free.nrw.commons.filepicker.UploadableFile
+import fr.free.nrw.commons.kvstore.JsonKvStore
+import fr.free.nrw.commons.location.LatLng
+import fr.free.nrw.commons.location.LocationPermissionsHelper
+import fr.free.nrw.commons.location.LocationPermissionsHelper.LocationPermissionCallback
+import fr.free.nrw.commons.location.LocationServiceManager
+import fr.free.nrw.commons.nearby.Place
+import fr.free.nrw.commons.upload.UploadActivity
+import fr.free.nrw.commons.utils.DialogUtil.showAlertDialog
+import fr.free.nrw.commons.utils.PermissionUtils.PERMISSIONS_STORAGE
+import fr.free.nrw.commons.utils.PermissionUtils.checkPermissionsAndPerformAction
+import fr.free.nrw.commons.utils.ViewUtil.showLongToast
+import fr.free.nrw.commons.utils.ViewUtil.showShortToast
+import fr.free.nrw.commons.wikidata.WikidataConstants.PLACE_OBJECT
+import java.util.Arrays
+import javax.inject.Inject
+import javax.inject.Named
+import javax.inject.Singleton
+
+@Singleton
+class ContributionController @Inject constructor(@param:Named("default_preferences") private val defaultKvStore: JsonKvStore) {
+ private var locationBeforeImageCapture: LatLng? = null
+ private var isInAppCameraUpload = false
+ @JvmField
+ var locationPermissionCallback: LocationPermissionCallback? = null
+ private var locationPermissionsHelper: LocationPermissionsHelper? = null
+
+ // Temporarily disabled, see issue [https://github.com/commons-app/apps-android-commons/issues/5847]
+ // LiveData> failedAndPendingContributionList;
+ @JvmField
+ var pendingContributionList: LiveData>? = null
+ @JvmField
+ var failedContributionList: LiveData>? = null
+
+ @JvmField
+ @Inject
+ var locationManager: LocationServiceManager? = null
+
+ @JvmField
+ @Inject
+ var repository: ContributionsRepository? = null
+
+ /**
+ * Check for permissions and initiate camera click
+ */
+ fun initiateCameraPick(
+ activity: Activity,
+ inAppCameraLocationPermissionLauncher: ActivityResultLauncher>,
+ resultLauncher: ActivityResultLauncher
+ ) {
+ val useExtStorage = defaultKvStore.getBoolean("useExternalStorage", true)
+ if (!useExtStorage) {
+ initiateCameraUpload(activity, resultLauncher)
+ return
+ }
+
+ checkPermissionsAndPerformAction(
+ activity,
+ {
+ if (defaultKvStore.getBoolean("inAppCameraFirstRun")) {
+ defaultKvStore.putBoolean("inAppCameraFirstRun", false)
+ askUserToAllowLocationAccess(
+ activity,
+ inAppCameraLocationPermissionLauncher,
+ resultLauncher
+ )
+ } else if (defaultKvStore.getBoolean("inAppCameraLocationPref")) {
+ createDialogsAndHandleLocationPermissions(
+ activity,
+ inAppCameraLocationPermissionLauncher, resultLauncher
+ )
+ } else {
+ initiateCameraUpload(activity, resultLauncher)
+ }
+ },
+ R.string.storage_permission_title,
+ R.string.write_storage_permission_rationale,
+ *PERMISSIONS_STORAGE
+ )
+ }
+
+ /**
+ * Asks users to provide location access
+ *
+ * @param activity
+ */
+ private fun createDialogsAndHandleLocationPermissions(
+ activity: Activity,
+ inAppCameraLocationPermissionLauncher: ActivityResultLauncher>?,
+ resultLauncher: ActivityResultLauncher
+ ) {
+ locationPermissionCallback = object : LocationPermissionCallback {
+ override fun onLocationPermissionDenied(toastMessage: String) {
+ Toast.makeText(
+ activity,
+ toastMessage,
+ Toast.LENGTH_LONG
+ ).show()
+ initiateCameraUpload(activity, resultLauncher)
+ }
+
+ override fun onLocationPermissionGranted() {
+ if (!locationPermissionsHelper!!.isLocationAccessToAppsTurnedOn()) {
+ showLocationOffDialog(
+ activity, R.string.in_app_camera_needs_location,
+ R.string.in_app_camera_location_unavailable, resultLauncher
+ )
+ } else {
+ initiateCameraUpload(activity, resultLauncher)
+ }
+ }
+ }
+
+ locationPermissionsHelper = LocationPermissionsHelper(
+ activity, locationManager!!, locationPermissionCallback
+ )
+ inAppCameraLocationPermissionLauncher?.launch(
+ arrayOf(permission.ACCESS_FINE_LOCATION)
+ )
+ }
+
+ /**
+ * Shows a dialog alerting the user about location services being off and asking them to turn it
+ * on
+ * TODO: Add a seperate callback in LocationPermissionsHelper for this.
+ * Ref: https://github.com/commons-app/apps-android-commons/pull/5494/files#r1510553114
+ *
+ * @param activity Activity reference
+ * @param dialogTextResource Resource id of text to be shown in dialog
+ * @param toastTextResource Resource id of text to be shown in toast
+ * @param resultLauncher
+ */
+ private fun showLocationOffDialog(
+ activity: Activity, dialogTextResource: Int,
+ toastTextResource: Int, resultLauncher: ActivityResultLauncher
+ ) {
+ showAlertDialog(activity,
+ activity.getString(R.string.ask_to_turn_location_on),
+ activity.getString(dialogTextResource),
+ activity.getString(R.string.title_app_shortcut_setting),
+ activity.getString(R.string.cancel),
+ { locationPermissionsHelper!!.openLocationSettings(activity) },
+ {
+ Toast.makeText(
+ activity, activity.getString(toastTextResource),
+ Toast.LENGTH_LONG
+ ).show()
+ initiateCameraUpload(activity, resultLauncher)
+ }
+ )
+ }
+
+ fun handleShowRationaleFlowCameraLocation(
+ activity: Activity,
+ inAppCameraLocationPermissionLauncher: ActivityResultLauncher>?,
+ resultLauncher: ActivityResultLauncher
+ ) {
+ showAlertDialog(
+ activity, activity.getString(R.string.location_permission_title),
+ activity.getString(R.string.in_app_camera_location_permission_rationale),
+ activity.getString(android.R.string.ok),
+ activity.getString(android.R.string.cancel),
+ {
+ createDialogsAndHandleLocationPermissions(
+ activity,
+ inAppCameraLocationPermissionLauncher, resultLauncher
+ )
+ },
+ {
+ locationPermissionCallback!!.onLocationPermissionDenied(
+ activity.getString(R.string.in_app_camera_location_permission_denied)
+ )
+ },
+ null
+ )
+ }
+
+ /**
+ * Suggest user to attach location information with pictures. If the user selects "Yes", then:
+ *
+ *
+ * Location is taken from the EXIF if the default camera application does not redact location
+ * tags.
+ *
+ *
+ * Otherwise, if the EXIF metadata does not have location information, then location captured by
+ * the app is used
+ *
+ * @param activity
+ */
+ private fun askUserToAllowLocationAccess(
+ activity: Activity,
+ inAppCameraLocationPermissionLauncher: ActivityResultLauncher>,
+ resultLauncher: ActivityResultLauncher
+ ) {
+ showAlertDialog(
+ activity,
+ activity.getString(R.string.in_app_camera_location_permission_title),
+ activity.getString(R.string.in_app_camera_location_access_explanation),
+ activity.getString(R.string.option_allow),
+ activity.getString(R.string.option_dismiss),
+ {
+ defaultKvStore.putBoolean("inAppCameraLocationPref", true)
+ createDialogsAndHandleLocationPermissions(
+ activity,
+ inAppCameraLocationPermissionLauncher, resultLauncher
+ )
+ },
+ {
+ showLongToast(activity, R.string.in_app_camera_location_permission_denied)
+ defaultKvStore.putBoolean("inAppCameraLocationPref", false)
+ initiateCameraUpload(activity, resultLauncher)
+ },
+ null
+ )
+ }
+
+ /**
+ * Initiate gallery picker
+ */
+ fun initiateGalleryPick(
+ activity: Activity,
+ resultLauncher: ActivityResultLauncher,
+ allowMultipleUploads: Boolean
+ ) {
+ initiateGalleryUpload(activity, resultLauncher, allowMultipleUploads)
+ }
+
+ /**
+ * Initiate gallery picker with permission
+ */
+ fun initiateCustomGalleryPickWithPermission(
+ activity: Activity,
+ resultLauncher: ActivityResultLauncher
+ ) {
+ setPickerConfiguration(activity, true)
+
+ checkPermissionsAndPerformAction(
+ activity,
+ { openCustomSelector(activity, resultLauncher, 0) },
+ R.string.storage_permission_title,
+ R.string.write_storage_permission_rationale,
+ *PERMISSIONS_STORAGE
+ )
+ }
+
+
+ /**
+ * Open chooser for gallery uploads
+ */
+ private fun initiateGalleryUpload(
+ activity: Activity, resultLauncher: ActivityResultLauncher,
+ allowMultipleUploads: Boolean
+ ) {
+ setPickerConfiguration(activity, allowMultipleUploads)
+ openGallery(activity, resultLauncher, 0, isDocumentPhotoPickerPreferred)
+ }
+
+ /**
+ * Sets configuration for file picker
+ */
+ private fun setPickerConfiguration(
+ activity: Activity,
+ allowMultipleUploads: Boolean
+ ) {
+ val copyToExternalStorage = defaultKvStore.getBoolean("useExternalStorage", true)
+ configuration(activity)
+ .setCopyTakenPhotosToPublicGalleryAppFolder(copyToExternalStorage)
+ .setAllowMultiplePickInGallery(allowMultipleUploads)
+ }
+
+ /**
+ * Initiate camera upload by opening camera
+ */
+ private fun initiateCameraUpload(
+ activity: Activity,
+ resultLauncher: ActivityResultLauncher
+ ) {
+ setPickerConfiguration(activity, false)
+ if (defaultKvStore.getBoolean("inAppCameraLocationPref", false)) {
+ locationBeforeImageCapture = locationManager!!.getLastLocation()
+ }
+ isInAppCameraUpload = true
+ openCameraForImage(activity, resultLauncher, 0)
+ }
+
+ private val isDocumentPhotoPickerPreferred: Boolean
+ get() = defaultKvStore.getBoolean(
+ "openDocumentPhotoPickerPref", true
+ )
+
+ fun onPictureReturnedFromGallery(
+ result: ActivityResult,
+ activity: Activity,
+ callbacks: FilePicker.Callbacks
+ ) {
+ if (isDocumentPhotoPickerPreferred) {
+ onPictureReturnedFromDocuments(result, activity, callbacks)
+ } else {
+ FilePicker.onPictureReturnedFromGallery(result, activity, callbacks)
+ }
+ }
+
+ fun onPictureReturnedFromCustomSelector(
+ result: ActivityResult,
+ activity: Activity,
+ callbacks: FilePicker.Callbacks
+ ) {
+ FilePicker.onPictureReturnedFromCustomSelector(result, activity, callbacks)
+ }
+
+ fun onPictureReturnedFromCamera(
+ result: ActivityResult,
+ activity: Activity,
+ callbacks: FilePicker.Callbacks
+ ) {
+ FilePicker.onPictureReturnedFromCamera(result, activity, callbacks)
+ }
+
+ /**
+ * Attaches callback for file picker.
+ */
+ fun handleActivityResultWithCallback(
+ activity: Activity,
+ handleActivityResult: HandleActivityResult
+ ) {
+ handleActivityResult.onHandleActivityResult(object : DefaultCallback() {
+ override fun onCanceled(source: FilePicker.ImageSource, type: Int) {
+ super.onCanceled(source, type)
+ defaultKvStore.remove(PLACE_OBJECT)
+ }
+
+ override fun onImagePickerError(
+ e: Exception, source: FilePicker.ImageSource,
+ type: Int
+ ) {
+ showShortToast(activity, R.string.error_occurred_in_picking_images)
+ }
+
+ override fun onImagesPicked(
+ imagesFiles: List,
+ source: FilePicker.ImageSource, type: Int
+ ) {
+ val intent = handleImagesPicked(activity, imagesFiles)
+ activity.startActivity(intent)
+ }
+ })
+ }
+
+ fun handleExternalImagesPicked(
+ activity: Activity,
+ data: Intent?
+ ): List {
+ return handleExternalImagesPicked(data, activity)
+ }
+
+ /**
+ * Returns intent to be passed to upload activity Attaches place object for nearby uploads and
+ * location before image capture if in-app camera is used
+ */
+ private fun handleImagesPicked(
+ context: Context,
+ imagesFiles: List
+ ): Intent {
+ val shareIntent = Intent(context, UploadActivity::class.java)
+ shareIntent.setAction(ACTION_INTERNAL_UPLOADS)
+ shareIntent
+ .putParcelableArrayListExtra(UploadActivity.EXTRA_FILES, ArrayList(imagesFiles))
+ val place = defaultKvStore.getJson(PLACE_OBJECT, Place::class.java)
+
+ if (place != null) {
+ shareIntent.putExtra(PLACE_OBJECT, place)
+ }
+
+ if (locationBeforeImageCapture != null) {
+ shareIntent.putExtra(
+ UploadActivity.LOCATION_BEFORE_IMAGE_CAPTURE,
+ locationBeforeImageCapture
+ )
+ }
+
+ shareIntent.putExtra(
+ UploadActivity.IN_APP_CAMERA_UPLOAD,
+ isInAppCameraUpload
+ )
+ isInAppCameraUpload = false // reset the flag for next use
+ return shareIntent
+ }
+
+ val pendingContributions: Unit
+ /**
+ * Fetches the contributions with the state "IN_PROGRESS", "QUEUED" and "PAUSED" and then it
+ * populates the `pendingContributionList`.
+ */
+ get() {
+ val pagedListConfig =
+ (PagedList.Config.Builder())
+ .setPrefetchDistance(50)
+ .setPageSize(10).build()
+ val factory = repository!!.fetchContributionsWithStates(
+ Arrays.asList(
+ Contribution.STATE_IN_PROGRESS, Contribution.STATE_QUEUED,
+ Contribution.STATE_PAUSED
+ )
+ )
+
+ val livePagedListBuilder = LivePagedListBuilder(factory, pagedListConfig)
+ pendingContributionList = livePagedListBuilder.build()
+ }
+
+ val failedContributions: Unit
+ /**
+ * Fetches the contributions with the state "FAILED" and populates the
+ * `failedContributionList`.
+ */
+ get() {
+ val pagedListConfig =
+ (PagedList.Config.Builder())
+ .setPrefetchDistance(50)
+ .setPageSize(10).build()
+ val factory = repository!!.fetchContributionsWithStates(
+ listOf(Contribution.STATE_FAILED)
+ )
+
+ val livePagedListBuilder = LivePagedListBuilder(factory, pagedListConfig)
+ failedContributionList = livePagedListBuilder.build()
+ }
+
+ /**
+ * Temporarily disabled, see issue [https://github.com/commons-app/apps-android-commons/issues/5847]
+ * 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 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();
+ // }
+
+ companion object {
+ const val ACTION_INTERNAL_UPLOADS: String = "internalImageUploads"
+ }
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionDao.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionDao.java
deleted file mode 100644
index 2e375145cc..0000000000
--- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionDao.java
+++ /dev/null
@@ -1,145 +0,0 @@
-package fr.free.nrw.commons.contributions;
-
-import android.database.sqlite.SQLiteException;
-import androidx.paging.DataSource;
-import androidx.room.Dao;
-import androidx.room.Delete;
-import androidx.room.Insert;
-import androidx.room.OnConflictStrategy;
-import androidx.room.Query;
-import androidx.room.Transaction;
-import androidx.room.Update;
-import io.reactivex.Completable;
-import io.reactivex.Single;
-import java.util.Calendar;
-import java.util.List;
-import timber.log.Timber;
-
-@Dao
-public abstract class ContributionDao {
-
- @Query("SELECT * FROM contribution order by media_dateUploaded DESC")
- abstract DataSource.Factory fetchContributions();
-
- @Insert(onConflict = OnConflictStrategy.REPLACE)
- public abstract void saveSynchronous(Contribution contribution);
-
- public Completable save(final Contribution contribution) {
- return Completable
- .fromAction(() -> {
- contribution.setDateModified(Calendar.getInstance().getTime());
- if (contribution.getDateUploadStarted() == null) {
- contribution.setDateUploadStarted(Calendar.getInstance().getTime());
- }
- saveSynchronous(contribution);
- });
- }
-
- @Transaction
- public void deleteAndSaveContribution(final Contribution oldContribution,
- final Contribution newContribution) {
- deleteSynchronous(oldContribution);
- saveSynchronous(newContribution);
- }
-
- @Insert(onConflict = OnConflictStrategy.REPLACE)
- public abstract Single> save(List contribution);
-
- @Delete
- public abstract void deleteSynchronous(Contribution contribution);
-
- /**
- * Deletes contributions with specific states from the database.
- *
- * @param states The states of the contributions to delete.
- * @throws SQLiteException If an SQLite error occurs.
- */
- @Query("DELETE FROM contribution WHERE state IN (:states)")
- public abstract void deleteContributionsWithStatesSynchronous(List states)
- throws SQLiteException;
-
- public Completable delete(final Contribution contribution) {
- return Completable
- .fromAction(() -> deleteSynchronous(contribution));
- }
-
- /**
- * Deletes contributions with specific states from the database.
- *
- * @param states The states of the contributions to delete.
- * @return A Completable indicating the result of the operation.
- */
- public Completable deleteContributionsWithStates(List states) {
- return Completable
- .fromAction(() -> deleteContributionsWithStatesSynchronous(states));
- }
-
- @Query("SELECT * from contribution WHERE media_filename=:fileName")
- public abstract List getContributionWithTitle(String fileName);
-
- @Query("SELECT * from contribution WHERE pageId=:pageId")
- public abstract Contribution getContribution(String pageId);
-
- @Query("SELECT * from contribution WHERE state IN (:states) order by media_dateUploaded DESC")
- public abstract Single> getContribution(List 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 getContributions(
- List 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 getContributionsSortedByDateUploadStarted(
- List states);
-
- @Query("SELECT COUNT(*) from contribution WHERE state in (:toUpdateStates)")
- public abstract Single getPendingUploads(int[] toUpdateStates);
-
- @Query("Delete FROM contribution")
- public abstract void deleteAll() throws SQLiteException;
-
- @Update
- public abstract void updateSynchronous(Contribution contribution);
-
- /**
- * Updates the state of contributions with specific states.
- *
- * @param states The current states of the contributions to update.
- * @param newState The new state to set.
- */
- @Query("UPDATE contribution SET state = :newState WHERE state IN (:states)")
- public abstract void updateContributionsState(List states, int newState);
-
- public Completable update(final Contribution contribution) {
- return Completable
- .fromAction(() -> {
- contribution.setDateModified(Calendar.getInstance().getTime());
- 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 states, int newState) {
- return Completable
- .fromAction(() -> {
- updateContributionsState(states, newState);
- });
- }
-}
diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionDao.kt b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionDao.kt
new file mode 100644
index 0000000000..50faa13408
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionDao.kt
@@ -0,0 +1,148 @@
+package fr.free.nrw.commons.contributions
+
+import android.database.sqlite.SQLiteException
+import androidx.paging.DataSource
+import androidx.room.Dao
+import androidx.room.Delete
+import androidx.room.Insert
+import androidx.room.OnConflictStrategy
+import androidx.room.Query
+import androidx.room.Transaction
+import androidx.room.Update
+import io.reactivex.Completable
+import io.reactivex.Single
+import java.util.Calendar
+
+@Dao
+abstract class ContributionDao {
+ @Query("SELECT * FROM contribution order by media_dateUploaded DESC")
+ abstract fun fetchContributions(): DataSource.Factory
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ abstract fun saveSynchronous(contribution: Contribution)
+
+ fun save(contribution: Contribution): Completable {
+ return Completable
+ .fromAction {
+ contribution.dateModified = Calendar.getInstance().time
+ if (contribution.dateUploadStarted == null) {
+ contribution.dateUploadStarted = Calendar.getInstance().time
+ }
+ saveSynchronous(contribution)
+ }
+ }
+
+ @Transaction
+ open fun deleteAndSaveContribution(
+ oldContribution: Contribution,
+ newContribution: Contribution
+ ) {
+ deleteSynchronous(oldContribution)
+ saveSynchronous(newContribution)
+ }
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ abstract fun save(contribution: List): Single>
+
+ @Delete
+ abstract fun 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)")
+ @Throws(SQLiteException::class)
+ abstract fun deleteContributionsWithStatesSynchronous(states: List)
+
+ fun delete(contribution: Contribution): Completable {
+ return Completable
+ .fromAction { deleteSynchronous(contribution) }
+ }
+
+ /**
+ * Deletes contributions with specific states from the database.
+ *
+ * @param states The states of the contributions to delete.
+ * @return A Completable indicating the result of the operation.
+ */
+ fun deleteContributionsWithStates(states: List): Completable {
+ return Completable
+ .fromAction { deleteContributionsWithStatesSynchronous(states) }
+ }
+
+ @Query("SELECT * from contribution WHERE media_filename=:fileName")
+ abstract fun getContributionWithTitle(fileName: String): List
+
+ @Query("SELECT * from contribution WHERE pageId=:pageId")
+ abstract fun getContribution(pageId: String): Contribution
+
+ @Query("SELECT * from contribution WHERE state IN (:states) order by media_dateUploaded DESC")
+ abstract fun getContribution(states: List): Single>
+
+ /**
+ * 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")
+ abstract fun getContributions(
+ states: List
+ ): DataSource.Factory
+
+ /**
+ * 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")
+ abstract fun getContributionsSortedByDateUploadStarted(
+ states: List
+ ): DataSource.Factory
+
+ @Query("SELECT COUNT(*) from contribution WHERE state in (:toUpdateStates)")
+ abstract fun getPendingUploads(toUpdateStates: IntArray): Single
+
+ @Query("Delete FROM contribution")
+ @Throws(SQLiteException::class)
+ abstract fun deleteAll()
+
+ @Update
+ abstract fun 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)")
+ abstract fun updateContributionsState(states: List, newState: Int)
+
+ fun update(contribution: Contribution): Completable {
+ return Completable.fromAction {
+ contribution.dateModified = Calendar.getInstance().time
+ 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.
+ */
+ fun updateContributionsWithStates(states: List, newState: Int): Completable {
+ return Completable
+ .fromAction {
+ updateContributionsState(states, newState)
+ }
+ }
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionViewHolder.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionViewHolder.java
deleted file mode 100644
index 568ac9a37e..0000000000
--- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionViewHolder.java
+++ /dev/null
@@ -1,171 +0,0 @@
-package fr.free.nrw.commons.contributions;
-
-import android.net.Uri;
-import android.text.TextUtils;
-import android.view.View;
-import android.webkit.URLUtil;
-import android.widget.ImageButton;
-import android.widget.ProgressBar;
-import android.widget.RelativeLayout;
-import android.widget.TextView;
-import androidx.annotation.Nullable;
-import androidx.appcompat.app.AlertDialog;
-import androidx.appcompat.app.AlertDialog.Builder;
-import androidx.recyclerview.widget.RecyclerView;
-import com.facebook.drawee.view.SimpleDraweeView;
-import com.facebook.imagepipeline.request.ImageRequest;
-import com.facebook.imagepipeline.request.ImageRequestBuilder;
-import fr.free.nrw.commons.R;
-import fr.free.nrw.commons.contributions.ContributionsListAdapter.Callback;
-import fr.free.nrw.commons.databinding.LayoutContributionBinding;
-import fr.free.nrw.commons.media.MediaClient;
-import io.reactivex.android.schedulers.AndroidSchedulers;
-import io.reactivex.disposables.CompositeDisposable;
-import io.reactivex.schedulers.Schedulers;
-import java.io.File;
-
-public class ContributionViewHolder extends RecyclerView.ViewHolder {
-
- private final Callback callback;
-
- LayoutContributionBinding binding;
-
- private int position;
- private Contribution contribution;
- private final CompositeDisposable compositeDisposable = new CompositeDisposable();
- private final MediaClient mediaClient;
- private boolean isWikipediaButtonDisplayed;
- private AlertDialog pausingPopUp;
- private View parent;
- private ImageRequest imageRequest;
-
- ContributionViewHolder(final View parent, final Callback callback,
- final MediaClient mediaClient) {
- super(parent);
- this.parent = parent;
- this.mediaClient = mediaClient;
- this.callback = callback;
-
- binding = LayoutContributionBinding.bind(parent);
-
- binding.contributionImage.setOnClickListener(v -> imageClicked());
- binding.wikipediaButton.setOnClickListener(v -> wikipediaButtonClicked());
-
- /* Set a dialog indicating that the upload is being paused. This is needed because pausing
- an upload might take a dozen seconds. */
- AlertDialog.Builder builder = new Builder(parent.getContext());
- builder.setCancelable(false);
- builder.setView(R.layout.progress_dialog);
- pausingPopUp = builder.create();
- }
-
- public void init(final int position, final Contribution contribution) {
-
- //handling crashes when the contribution is null.
- if (null == contribution) {
- return;
- }
-
- this.contribution = contribution;
- this.position = position;
- binding.contributionTitle.setText(contribution.getMedia().getMostRelevantCaption());
- binding.authorView.setText(contribution.getMedia().getAuthor());
-
- //Removes flicker of loading image.
- binding.contributionImage.getHierarchy().setFadeDuration(0);
-
- binding.contributionImage.getHierarchy().setPlaceholderImage(R.drawable.image_placeholder);
- binding.contributionImage.getHierarchy().setFailureImage(R.drawable.image_placeholder);
-
- final String imageSource = chooseImageSource(contribution.getMedia().getThumbUrl(),
- contribution.getLocalUri());
- if (!TextUtils.isEmpty(imageSource)) {
- if (URLUtil.isHttpsUrl(imageSource)) {
- imageRequest = ImageRequestBuilder.newBuilderWithSource(Uri.parse(imageSource))
- .setProgressiveRenderingEnabled(true)
- .build();
- } else if (URLUtil.isFileUrl(imageSource)) {
- imageRequest = ImageRequest.fromUri(Uri.parse(imageSource));
- } else if (imageSource != null) {
- final File file = new File(imageSource);
- imageRequest = ImageRequest.fromFile(file);
- }
-
- if (imageRequest != null) {
- binding.contributionImage.setImageRequest(imageRequest);
- }
- }
-
- binding.contributionSequenceNumber.setText(String.valueOf(position + 1));
- binding.contributionSequenceNumber.setVisibility(View.VISIBLE);
- binding.wikipediaButton.setVisibility(View.GONE);
- binding.contributionState.setVisibility(View.GONE);
- binding.contributionProgress.setVisibility(View.GONE);
- binding.imageOptions.setVisibility(View.GONE);
- binding.contributionState.setText("");
- checkIfMediaExistsOnWikipediaPage(contribution);
-
- }
-
- /**
- * Checks if a media exists on the corresponding Wikipedia article Currently the check is made
- * for the device's current language Wikipedia
- *
- * @param contribution
- */
- private void checkIfMediaExistsOnWikipediaPage(final Contribution contribution) {
- if (contribution.getWikidataPlace() == null
- || contribution.getWikidataPlace().getWikipediaArticle() == null) {
- return;
- }
- final String wikipediaArticle = contribution.getWikidataPlace().getWikipediaPageTitle();
- compositeDisposable.add(mediaClient.doesPageContainMedia(wikipediaArticle)
- .subscribeOn(Schedulers.io())
- .observeOn(AndroidSchedulers.mainThread())
- .subscribe(mediaExists -> {
- displayWikipediaButton(mediaExists);
- }));
- }
-
- /**
- * Handle action buttons visibility if the corresponding wikipedia page doesn't contain any
- * media. This method needs to control the state of just the scenario where media does not
- * exists as other scenarios are already handled in the init method.
- *
- * @param mediaExists
- */
- private void displayWikipediaButton(Boolean mediaExists) {
- if (!mediaExists) {
- binding.wikipediaButton.setVisibility(View.VISIBLE);
- isWikipediaButtonDisplayed = true;
- binding.imageOptions.setVisibility(View.VISIBLE);
- }
- }
-
- /**
- * Returns the image source for the image view, first preference is given to thumbUrl if that is
- * null, moves to local uri and if both are null return null
- *
- * @param thumbUrl
- * @param localUri
- * @return
- */
- @Nullable
- private String chooseImageSource(final String thumbUrl, final Uri localUri) {
- return !TextUtils.isEmpty(thumbUrl) ? thumbUrl :
- localUri != null ? localUri.toString() :
- null;
- }
-
- public void imageClicked() {
- callback.openMediaDetail(position, isWikipediaButtonDisplayed);
- }
-
- public void wikipediaButtonClicked() {
- callback.addImageToWikipedia(contribution);
- }
-
- public ImageRequest getImageRequest() {
- return imageRequest;
- }
-}
diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionViewHolder.kt b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionViewHolder.kt
new file mode 100644
index 0000000000..899ef458f6
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionViewHolder.kt
@@ -0,0 +1,182 @@
+package fr.free.nrw.commons.contributions
+
+import android.net.Uri
+import android.text.TextUtils
+import android.view.View
+import android.webkit.URLUtil
+import androidx.appcompat.app.AlertDialog
+import androidx.recyclerview.widget.RecyclerView
+import com.facebook.imagepipeline.request.ImageRequest
+import com.facebook.imagepipeline.request.ImageRequestBuilder
+import fr.free.nrw.commons.Media
+import fr.free.nrw.commons.utils.MediaAttributionUtil
+import fr.free.nrw.commons.MediaDataExtractor
+import fr.free.nrw.commons.R
+import fr.free.nrw.commons.databinding.LayoutContributionBinding
+import fr.free.nrw.commons.media.MediaClient
+import io.reactivex.android.schedulers.AndroidSchedulers
+import io.reactivex.disposables.CompositeDisposable
+import io.reactivex.schedulers.Schedulers
+import timber.log.Timber
+import java.io.File
+
+class ContributionViewHolder internal constructor(
+ parent: View,
+ private val callback: ContributionsListAdapter.Callback,
+ private val compositeDisposable: CompositeDisposable,
+ private val mediaClient: MediaClient,
+ private val mediaDataExtractor: MediaDataExtractor
+) : RecyclerView.ViewHolder(parent) {
+ var binding: LayoutContributionBinding = LayoutContributionBinding.bind(parent)
+
+ private var position = 0
+ private var contribution: Contribution? = null
+ private var isWikipediaButtonDisplayed = false
+ private val pausingPopUp: AlertDialog
+ var imageRequest: ImageRequest? = null
+ private set
+
+ init {
+ binding.contributionImage.setOnClickListener { v: View? -> imageClicked() }
+ binding.wikipediaButton.setOnClickListener { v: View? -> wikipediaButtonClicked() }
+
+ /* Set a dialog indicating that the upload is being paused. This is needed because pausing
+an upload might take a dozen seconds. */
+ val builder = AlertDialog.Builder(
+ parent.context
+ )
+ builder.setCancelable(false)
+ builder.setView(R.layout.progress_dialog)
+ pausingPopUp = builder.create()
+ }
+
+ fun init(position: Int, contribution: Contribution?) {
+ //handling crashes when the contribution is null.
+
+ if (null == contribution) {
+ return
+ }
+
+ this.contribution = contribution
+ this.position = position
+ binding.contributionTitle.text = contribution.media.mostRelevantCaption
+ setAuthorText(contribution.media)
+
+ //Removes flicker of loading image.
+ binding.contributionImage.hierarchy.fadeDuration = 0
+
+ binding.contributionImage.hierarchy.setPlaceholderImage(R.drawable.image_placeholder)
+ binding.contributionImage.hierarchy.setFailureImage(R.drawable.image_placeholder)
+
+ val imageSource = chooseImageSource(
+ contribution.media.thumbUrl,
+ contribution.localUri
+ )
+ if (!TextUtils.isEmpty(imageSource)) {
+ if (URLUtil.isHttpsUrl(imageSource)) {
+ imageRequest = ImageRequestBuilder.newBuilderWithSource(Uri.parse(imageSource))
+ .setProgressiveRenderingEnabled(true)
+ .build()
+ } else 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) {
+ binding.contributionImage.setImageRequest(imageRequest)
+ }
+ }
+
+ binding.contributionSequenceNumber.text = (position + 1).toString()
+ binding.contributionSequenceNumber.visibility = View.VISIBLE
+ binding.wikipediaButton.visibility = View.GONE
+ binding.contributionState.visibility = View.GONE
+ binding.contributionProgress.visibility = View.GONE
+ binding.imageOptions.visibility = View.GONE
+ binding.contributionState.text = ""
+ checkIfMediaExistsOnWikipediaPage(contribution)
+ }
+
+ fun updateAttribution() {
+ if (contribution != null) {
+ val media = contribution!!.media
+ if (!media.getAttributedAuthor().isNullOrEmpty()) {
+ return
+ }
+ compositeDisposable.addAll(
+ mediaDataExtractor.fetchCreatorIdsAndLabels(media)
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(
+ { idAndLabels ->
+ media.creatorName = MediaAttributionUtil.getCreatorName(idAndLabels)
+ setAuthorText(media)
+ },
+ { t: Throwable? -> Timber.e(t) })
+ )
+ }
+ }
+
+ private fun setAuthorText(media: Media) {
+ binding.authorView.text = MediaAttributionUtil.getTagLine(media, itemView.context)
+ }
+
+ /**
+ * Checks if a media exists on the corresponding Wikipedia article Currently the check is made
+ * for the device's current language Wikipedia
+ *
+ * @param contribution
+ */
+ private fun checkIfMediaExistsOnWikipediaPage(contribution: Contribution) {
+ if (contribution.wikidataPlace == null
+ || contribution.wikidataPlace!!.wikipediaArticle == null
+ ) {
+ return
+ }
+ val wikipediaArticle = contribution.wikidataPlace!!.getWikipediaPageTitle()
+ compositeDisposable.add(
+ mediaClient.doesPageContainMedia(wikipediaArticle)
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe { mediaExists: Boolean ->
+ displayWikipediaButton(mediaExists)
+ })
+ }
+
+ /**
+ * Handle action buttons visibility if the corresponding wikipedia page doesn't contain any
+ * media. This method needs to control the state of just the scenario where media does not
+ * exists as other scenarios are already handled in the init method.
+ *
+ * @param mediaExists
+ */
+ private fun displayWikipediaButton(mediaExists: Boolean) {
+ if (!mediaExists) {
+ binding.wikipediaButton.visibility = View.VISIBLE
+ isWikipediaButtonDisplayed = true
+ binding.imageOptions.visibility = View.VISIBLE
+ }
+ }
+
+ /**
+ * Returns the image source for the image view, first preference is given to thumbUrl if that is
+ * null, moves to local uri and if both are null return null
+ *
+ * @param thumbUrl
+ * @param localUri
+ * @return
+ */
+ private fun chooseImageSource(thumbUrl: String?, localUri: Uri?): String? {
+ return if (!TextUtils.isEmpty(thumbUrl)) thumbUrl else localUri?.toString()
+ }
+
+ fun imageClicked() {
+ callback.openMediaDetail(position, isWikipediaButtonDisplayed)
+ }
+
+ fun wikipediaButtonClicked() {
+ callback.addImageToWikipedia(contribution)
+ }
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsContract.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsContract.java
deleted file mode 100644
index 4397803328..0000000000
--- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsContract.java
+++ /dev/null
@@ -1,23 +0,0 @@
-package fr.free.nrw.commons.contributions;
-
-import android.content.Context;
-import fr.free.nrw.commons.BasePresenter;
-
-/**
- * The contract for Contributions View & Presenter
- */
-public class ContributionsContract {
-
- public interface View {
-
- void showMessage(String localizedMessage);
-
- Context getContext();
- }
-
- public interface UserActionListener extends BasePresenter {
-
- Contribution getContributionsWithTitle(String uri);
-
- }
-}
diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsContract.kt b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsContract.kt
new file mode 100644
index 0000000000..7027950e37
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsContract.kt
@@ -0,0 +1,19 @@
+package fr.free.nrw.commons.contributions
+
+import android.content.Context
+import fr.free.nrw.commons.BasePresenter
+
+/**
+ * The contract for Contributions View & Presenter
+ */
+interface ContributionsContract {
+
+ interface View {
+ fun showMessage(localizedMessage: String)
+ fun getContext(): Context?
+ }
+
+ interface UserActionListener : BasePresenter {
+ fun getContributionsWithTitle(uri: String): Contribution
+ }
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsFragment.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsFragment.java
deleted file mode 100644
index a840aa8e11..0000000000
--- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsFragment.java
+++ /dev/null
@@ -1,943 +0,0 @@
-package fr.free.nrw.commons.contributions;
-
-import static android.content.Context.SENSOR_SERVICE;
-import static fr.free.nrw.commons.contributions.Contribution.STATE_FAILED;
-import static fr.free.nrw.commons.contributions.Contribution.STATE_PAUSED;
-import static fr.free.nrw.commons.nearby.fragments.NearbyParentFragment.WLM_URL;
-import static fr.free.nrw.commons.profile.ProfileActivity.KEY_USERNAME;
-import static fr.free.nrw.commons.utils.ImageUtils.IMAGE_OK;
-import static fr.free.nrw.commons.utils.LengthUtils.computeBearing;
-import static fr.free.nrw.commons.utils.LengthUtils.formatDistanceBetween;
-
-import android.Manifest;
-import android.Manifest.permission;
-import android.annotation.SuppressLint;
-import android.content.Context;
-import android.content.Intent;
-import android.hardware.Sensor;
-import android.hardware.SensorEvent;
-import android.hardware.SensorEventListener;
-import android.hardware.SensorManager;
-import android.os.Bundle;
-import android.view.LayoutInflater;
-import android.view.Menu;
-import android.view.MenuInflater;
-import android.view.MenuItem;
-import android.view.MenuItem.OnMenuItemClickListener;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.CheckBox;
-import android.widget.ImageView;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-import android.widget.Toast;
-import androidx.activity.result.ActivityResultCallback;
-import androidx.activity.result.ActivityResultLauncher;
-import androidx.activity.result.contract.ActivityResultContracts;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.fragment.app.Fragment;
-import androidx.fragment.app.FragmentManager.OnBackStackChangedListener;
-import androidx.fragment.app.FragmentTransaction;
-import fr.free.nrw.commons.CommonsApplication;
-import fr.free.nrw.commons.Utils;
-import fr.free.nrw.commons.auth.SessionManager;
-import fr.free.nrw.commons.databinding.FragmentContributionsBinding;
-import fr.free.nrw.commons.notification.models.Notification;
-import fr.free.nrw.commons.notification.NotificationController;
-import fr.free.nrw.commons.profile.ProfileActivity;
-import fr.free.nrw.commons.theme.BaseActivity;
-import fr.free.nrw.commons.upload.UploadProgressActivity;
-import java.util.Calendar;
-import java.util.Date;
-import java.util.List;
-import java.util.Map;
-import javax.inject.Inject;
-import javax.inject.Named;
-import androidx.work.WorkManager;
-import fr.free.nrw.commons.Media;
-import fr.free.nrw.commons.R;
-import fr.free.nrw.commons.campaigns.models.Campaign;
-import fr.free.nrw.commons.campaigns.CampaignView;
-import fr.free.nrw.commons.campaigns.CampaignsPresenter;
-import fr.free.nrw.commons.campaigns.ICampaignsView;
-import fr.free.nrw.commons.contributions.ContributionsListFragment.Callback;
-import fr.free.nrw.commons.contributions.MainActivity.ActiveFragment;
-import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
-import fr.free.nrw.commons.kvstore.JsonKvStore;
-import fr.free.nrw.commons.location.LatLng;
-import fr.free.nrw.commons.location.LocationServiceManager;
-import fr.free.nrw.commons.location.LocationUpdateListener;
-import fr.free.nrw.commons.media.MediaDetailPagerFragment;
-import fr.free.nrw.commons.media.MediaDetailPagerFragment.MediaDetailProvider;
-import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
-import fr.free.nrw.commons.nearby.NearbyController;
-import fr.free.nrw.commons.nearby.NearbyNotificationCardView;
-import fr.free.nrw.commons.nearby.Place;
-import fr.free.nrw.commons.notification.NotificationActivity;
-import fr.free.nrw.commons.upload.worker.UploadWorker;
-import fr.free.nrw.commons.utils.ConfigUtils;
-import fr.free.nrw.commons.utils.DialogUtil;
-import fr.free.nrw.commons.utils.NetworkUtils;
-import fr.free.nrw.commons.utils.PermissionUtils;
-import fr.free.nrw.commons.utils.ViewUtil;
-import io.reactivex.Observable;
-import io.reactivex.android.schedulers.AndroidSchedulers;
-import io.reactivex.disposables.CompositeDisposable;
-import io.reactivex.schedulers.Schedulers;
-import timber.log.Timber;
-
-public class ContributionsFragment
- extends CommonsDaggerSupportFragment
- implements
- OnBackStackChangedListener,
- LocationUpdateListener,
- MediaDetailProvider,
- SensorEventListener,
- ICampaignsView, ContributionsContract.View, Callback {
-
- @Inject
- @Named("default_preferences")
- JsonKvStore store;
- @Inject
- NearbyController nearbyController;
- @Inject
- OkHttpJsonApiClient okHttpJsonApiClient;
- @Inject
- CampaignsPresenter presenter;
- @Inject
- LocationServiceManager locationManager;
- @Inject
- NotificationController notificationController;
- @Inject
- ContributionController contributionController;
-
- private CompositeDisposable compositeDisposable = new CompositeDisposable();
-
- private ContributionsListFragment contributionsListFragment;
- private static final String CONTRIBUTION_LIST_FRAGMENT_TAG = "ContributionListFragmentTag";
- private MediaDetailPagerFragment mediaDetailPagerFragment;
- static final String MEDIA_DETAIL_PAGER_FRAGMENT_TAG = "MediaDetailFragmentTag";
- private static final int MAX_RETRIES = 10;
-
- public FragmentContributionsBinding binding;
-
- @Inject
- ContributionsPresenter contributionsPresenter;
-
- @Inject
- SessionManager sessionManager;
-
- private LatLng currentLatLng;
-
- private boolean isFragmentAttachedBefore = false;
- private View checkBoxView;
- private CheckBox checkBox;
-
- public TextView notificationCount;
-
- public TextView pendingUploadsCountTextView;
-
- public TextView uploadsErrorTextView;
-
- public ImageView pendingUploadsImageView;
-
- private Campaign wlmCampaign;
-
- String userName;
- private boolean isUserProfile;
-
- private SensorManager mSensorManager;
- private Sensor mLight;
- private float direction;
- private ActivityResultLauncher nearbyLocationPermissionLauncher = registerForActivityResult(
- new ActivityResultContracts.RequestMultiplePermissions(),
- new ActivityResultCallback