From 2c8c441f2564916f4519879558a2645209c9300a Mon Sep 17 00:00:00 2001 From: Paul Hawke Date: Fri, 13 Dec 2024 08:15:13 -0600 Subject: [PATCH] Convert upload to kotlin (part 1) (#6024) * Convert upload dagger module to kotlin * Code cleanup and convert the upload contract to kotlin * Code cleanup and convert CategoriesContract to kotlin * Code cleanup and convert MediaLicenseContract to kotlin * Code cleanup and convert UploadMediaDetailsContract to kotlin * Code cleanup, fixed nullability and converted DepictsContract to kotlin * Removed unused class * Convert FileMetadataUtils to kotlin * Convert EXIFReader to kotlin * Convert FileUtils to kotlin * Convert FileUtilsWrapper to kotlin * Convert ImageProcessingService to kotlin * Convert PageContentsCreator to kotlin * Convert PendingUploadsPresenter and contract to Kotlin with some code-cleanup * Convert ReadFBMD to kotlin * Convert SimilarImageInterface to kotlin * Removed unused classes * Fix merge/rebase issue --------- Co-authored-by: Nicolas Raoul --- .../commons/di/CommonsApplicationModule.kt | 6 + .../fragments/NearbyParentFragment.java | 4 +- .../free/nrw/commons/upload/EXIFReader.java | 36 --- .../fr/free/nrw/commons/upload/EXIFReader.kt | 31 +++ .../commons/upload/FailedUploadsFragment.kt | 77 +++-- .../nrw/commons/upload/FileMetadataUtils.java | 59 ---- .../nrw/commons/upload/FileMetadataUtils.kt | 61 ++++ .../fr/free/nrw/commons/upload/FileUtils.java | 183 ------------ .../fr/free/nrw/commons/upload/FileUtils.kt | 159 +++++++++++ .../nrw/commons/upload/FileUtilsWrapper.java | 94 ------- .../nrw/commons/upload/FileUtilsWrapper.kt | 85 ++++++ .../upload/ImageProcessingService.java | 190 ------------- .../commons/upload/ImageProcessingService.kt | 183 ++++++++++++ .../commons/upload/PageContentsCreator.java | 127 --------- .../nrw/commons/upload/PageContentsCreator.kt | 106 +++++++ .../upload/PendingUploadsContract.java | 31 --- .../commons/upload/PendingUploadsContract.kt | 26 ++ .../commons/upload/PendingUploadsFragment.kt | 90 +++--- .../upload/PendingUploadsPresenter.java | 263 ------------------ .../commons/upload/PendingUploadsPresenter.kt | 256 +++++++++++++++++ .../fr/free/nrw/commons/upload/ReadFBMD.java | 48 ---- .../fr/free/nrw/commons/upload/ReadFBMD.kt | 45 +++ .../commons/upload/SimilarImageInterface.java | 6 - .../commons/upload/SimilarImageInterface.kt | 9 + .../upload/ThumbnailClickedListener.java | 7 - .../nrw/commons/upload/UploadActivity.java | 130 +++++---- .../free/nrw/commons/upload/UploadClient.kt | 13 +- .../nrw/commons/upload/UploadContract.java | 82 ------ .../free/nrw/commons/upload/UploadContract.kt | 77 +++++ .../free/nrw/commons/upload/UploadModel.java | 2 +- .../free/nrw/commons/upload/UploadModule.java | 58 ---- .../free/nrw/commons/upload/UploadModule.kt | 33 +++ .../free/nrw/commons/upload/UploadView.java | 88 ------ .../upload/categories/CategoriesContract.java | 97 ------- .../upload/categories/CategoriesContract.kt | 88 ++++++ .../upload/categories/CategoriesPresenter.kt | 9 +- .../categories/UploadCategoriesFragment.java | 31 ++- ...epictsContract.java => DepictsContract.kt} | 66 +++-- .../upload/depicts/DepictsFragment.java | 2 +- .../upload/depicts/DepictsPresenter.kt | 7 +- .../upload/license/MediaLicenseContract.java | 28 -- .../upload/license/MediaLicenseContract.kt | 24 ++ .../upload/license/MediaLicenseFragment.java | 2 +- .../upload/license/MediaLicensePresenter.java | 15 +- .../UploadMediaDetailsContract.java | 115 -------- .../UploadMediaDetailsContract.kt | 122 ++++++++ .../mediaDetails/UploadMediaPresenter.java | 114 ++++---- .../depictions/UploadDepictsCallback.java | 8 - .../fr/free/nrw/commons/utils/ConfigUtils.kt | 3 + .../fr/free/nrw/commons/utils/TimeProvider.kt | 5 + .../commons/upload/UploadActivityUnitTests.kt | 5 +- .../nrw/commons/upload/UploadClientTest.kt | 3 +- .../nrw/commons/upload/UploadPresenterTest.kt | 14 +- .../UploadCategoriesFragmentUnitTests.kt | 5 +- .../depicts/DepictsFragmentUnitTests.kt | 2 +- 55 files changed, 1595 insertions(+), 1835 deletions(-) delete mode 100644 app/src/main/java/fr/free/nrw/commons/upload/EXIFReader.java create mode 100644 app/src/main/java/fr/free/nrw/commons/upload/EXIFReader.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/upload/FileMetadataUtils.java create mode 100644 app/src/main/java/fr/free/nrw/commons/upload/FileMetadataUtils.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/upload/FileUtils.java create mode 100644 app/src/main/java/fr/free/nrw/commons/upload/FileUtils.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/upload/FileUtilsWrapper.java create mode 100644 app/src/main/java/fr/free/nrw/commons/upload/FileUtilsWrapper.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/upload/ImageProcessingService.java create mode 100644 app/src/main/java/fr/free/nrw/commons/upload/ImageProcessingService.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/upload/PageContentsCreator.java create mode 100644 app/src/main/java/fr/free/nrw/commons/upload/PageContentsCreator.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/upload/PendingUploadsContract.java create mode 100644 app/src/main/java/fr/free/nrw/commons/upload/PendingUploadsContract.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/upload/PendingUploadsPresenter.java create mode 100644 app/src/main/java/fr/free/nrw/commons/upload/PendingUploadsPresenter.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/upload/ReadFBMD.java create mode 100644 app/src/main/java/fr/free/nrw/commons/upload/ReadFBMD.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/upload/SimilarImageInterface.java create mode 100644 app/src/main/java/fr/free/nrw/commons/upload/SimilarImageInterface.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/upload/ThumbnailClickedListener.java delete mode 100644 app/src/main/java/fr/free/nrw/commons/upload/UploadContract.java create mode 100644 app/src/main/java/fr/free/nrw/commons/upload/UploadContract.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/upload/UploadModule.java create mode 100644 app/src/main/java/fr/free/nrw/commons/upload/UploadModule.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/upload/UploadView.java delete mode 100644 app/src/main/java/fr/free/nrw/commons/upload/categories/CategoriesContract.java create mode 100644 app/src/main/java/fr/free/nrw/commons/upload/categories/CategoriesContract.kt rename app/src/main/java/fr/free/nrw/commons/upload/depicts/{DepictsContract.java => DepictsContract.kt} (56%) delete mode 100644 app/src/main/java/fr/free/nrw/commons/upload/license/MediaLicenseContract.java create mode 100644 app/src/main/java/fr/free/nrw/commons/upload/license/MediaLicenseContract.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailsContract.java create mode 100644 app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailsContract.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/upload/structure/depictions/UploadDepictsCallback.java create mode 100644 app/src/main/java/fr/free/nrw/commons/utils/TimeProvider.kt diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.kt b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.kt index 6f883769f..b195674a9 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.kt +++ b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.kt @@ -29,6 +29,7 @@ import fr.free.nrw.commons.settings.Prefs import fr.free.nrw.commons.upload.UploadController import fr.free.nrw.commons.upload.depicts.DepictsDao import fr.free.nrw.commons.utils.ConfigUtils.isBetaFlavour +import fr.free.nrw.commons.utils.TimeProvider import fr.free.nrw.commons.wikidata.WikidataEditListener import fr.free.nrw.commons.wikidata.WikidataEditListenerImpl import io.reactivex.Scheduler @@ -224,6 +225,11 @@ open class CommonsApplicationModule(private val applicationContext: Context) { fun providesContentResolver(context: Context): ContentResolver = context.contentResolver + @Provides + fun provideTimeProvider(): TimeProvider { + return TimeProvider(System::currentTimeMillis) + } + companion object { const val IO_THREAD: String = "io_thread" const val MAIN_THREAD: String = "main_thread" diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.java b/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.java index eb42606ba..fff4e4ca7 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.java @@ -528,7 +528,9 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment final Bundle bundle = new Bundle(); try { bundle.putString("query", - FileUtils.readFromResource("/queries/radius_query_for_upload_wizard.rq")); + FileUtils.INSTANCE.readFromResource( + "/queries/radius_query_for_upload_wizard.rq") + ); } catch (IOException e) { Timber.e(e); } diff --git a/app/src/main/java/fr/free/nrw/commons/upload/EXIFReader.java b/app/src/main/java/fr/free/nrw/commons/upload/EXIFReader.java deleted file mode 100644 index 0dd13acea..000000000 --- a/app/src/main/java/fr/free/nrw/commons/upload/EXIFReader.java +++ /dev/null @@ -1,36 +0,0 @@ -package fr.free.nrw.commons.upload; - -import androidx.exifinterface.media.ExifInterface; - -import javax.inject.Inject; -import javax.inject.Singleton; - -import fr.free.nrw.commons.utils.ImageUtils; -import io.reactivex.Single; - -/** - * We try to minimize uploads from the Commons app that might be copyright violations. - * If an image does not have any Exif metadata, then it was likely downloaded from the internet, - * and is probably not an original work by the user. We detect these kinds of images by looking - * for the presence of some basic Exif metadata. - */ -@Singleton -public class EXIFReader { - @Inject - public EXIFReader() { - } - - public Single processMetadata(String path) { - try { - ExifInterface exif = new ExifInterface(path); - if (exif.getAttribute(ExifInterface.TAG_MAKE) != null - || exif.getAttribute(ExifInterface.TAG_DATETIME) != null) { - return Single.just(ImageUtils.IMAGE_OK); - } - } catch (Exception e) { - return Single.just(ImageUtils.FILE_NO_EXIF); - } - return Single.just(ImageUtils.FILE_NO_EXIF); - } -} - diff --git a/app/src/main/java/fr/free/nrw/commons/upload/EXIFReader.kt b/app/src/main/java/fr/free/nrw/commons/upload/EXIFReader.kt new file mode 100644 index 000000000..f97052065 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/upload/EXIFReader.kt @@ -0,0 +1,31 @@ +package fr.free.nrw.commons.upload + +import androidx.exifinterface.media.ExifInterface +import androidx.exifinterface.media.ExifInterface.TAG_DATETIME +import androidx.exifinterface.media.ExifInterface.TAG_MAKE +import fr.free.nrw.commons.utils.ImageUtils.FILE_NO_EXIF +import fr.free.nrw.commons.utils.ImageUtils.IMAGE_OK +import io.reactivex.Single +import javax.inject.Inject +import javax.inject.Singleton + +/** + * We try to minimize uploads from the Commons app that might be copyright violations. + * If an image does not have any Exif metadata, then it was likely downloaded from the internet, + * and is probably not an original work by the user. We detect these kinds of images by looking + * for the presence of some basic Exif metadata. + */ +@Singleton +class EXIFReader @Inject constructor() { + fun processMetadata(path: String): Single = Single.just( + try { + if (ExifInterface(path).hasMakeOrDate) IMAGE_OK else FILE_NO_EXIF + } catch (e: Exception) { + FILE_NO_EXIF + } + ) + + private val ExifInterface.hasMakeOrDate get() = + getAttribute(TAG_MAKE) != null || getAttribute(TAG_DATETIME) != null +} + diff --git a/app/src/main/java/fr/free/nrw/commons/upload/FailedUploadsFragment.kt b/app/src/main/java/fr/free/nrw/commons/upload/FailedUploadsFragment.kt index dbbab7359..6e0712ea9 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/FailedUploadsFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/FailedUploadsFragment.kt @@ -10,6 +10,7 @@ import androidx.recyclerview.widget.LinearLayoutManager 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.contributions.Contribution.Companion.STATE_FAILED import fr.free.nrw.commons.databinding.FragmentFailedUploadsBinding import fr.free.nrw.commons.di.CommonsDaggerSupportFragment import fr.free.nrw.commons.media.MediaClient @@ -43,7 +44,7 @@ class FailedUploadsFragment : private lateinit var adapter: FailedUploadsAdapter - var contributionsList = ArrayList() + var contributionsList = mutableListOf() private lateinit var uploadProgressActivity: UploadProgressActivity @@ -71,7 +72,7 @@ class FailedUploadsFragment : inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?, - ): View? { + ): View { binding = FragmentFailedUploadsBinding.inflate(layoutInflater) pendingUploadsPresenter.onAttachView(this) initAdapter() @@ -99,9 +100,9 @@ class FailedUploadsFragment : pendingUploadsPresenter.getFailedContributions() pendingUploadsPresenter.failedContributionList.observe( viewLifecycleOwner, - ) { list: PagedList -> + ) { list: PagedList -> adapter.submitList(list) - contributionsList = ArrayList() + contributionsList = mutableListOf() list.forEach { if (it != null) { contributionsList.add(it) @@ -124,26 +125,22 @@ class FailedUploadsFragment : * Restarts all the failed uploads. */ fun restartUploads() { - if (contributionsList != null) { - pendingUploadsPresenter.restartUploads( - contributionsList, - 0, - this.requireContext().applicationContext, - ) - } + pendingUploadsPresenter.restartUploads( + contributionsList, + 0, + requireContext().applicationContext, + ) } /** * Restarts a specific upload. */ override fun restartUpload(index: Int) { - if (contributionsList != null) { - pendingUploadsPresenter.restartUpload( - contributionsList, - index, - this.requireContext().applicationContext, - ) - } + pendingUploadsPresenter.restartUpload( + contributionsList, + index, + requireContext().applicationContext, + ) } /** @@ -166,7 +163,7 @@ class FailedUploadsFragment : ViewUtil.showShortToast(context, R.string.cancelling_upload) pendingUploadsPresenter.deleteUpload( contribution, - this.requireContext().applicationContext, + requireContext().applicationContext, ) }, {}, @@ -177,28 +174,24 @@ class FailedUploadsFragment : * 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), - ) - }, - {}, - ) - } + 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(STATE_FAILED)) + }, + {}, + ) } } diff --git a/app/src/main/java/fr/free/nrw/commons/upload/FileMetadataUtils.java b/app/src/main/java/fr/free/nrw/commons/upload/FileMetadataUtils.java deleted file mode 100644 index 3a6db30a7..000000000 --- a/app/src/main/java/fr/free/nrw/commons/upload/FileMetadataUtils.java +++ /dev/null @@ -1,59 +0,0 @@ -package fr.free.nrw.commons.upload; - -import timber.log.Timber; - -import static androidx.exifinterface.media.ExifInterface.TAG_ARTIST; -import static androidx.exifinterface.media.ExifInterface.TAG_BODY_SERIAL_NUMBER; -import static androidx.exifinterface.media.ExifInterface.TAG_CAMERA_OWNER_NAME; -import static androidx.exifinterface.media.ExifInterface.TAG_COPYRIGHT; -import static androidx.exifinterface.media.ExifInterface.TAG_GPS_ALTITUDE; -import static androidx.exifinterface.media.ExifInterface.TAG_GPS_ALTITUDE_REF; -import static androidx.exifinterface.media.ExifInterface.TAG_GPS_LATITUDE; -import static androidx.exifinterface.media.ExifInterface.TAG_GPS_LATITUDE_REF; -import static androidx.exifinterface.media.ExifInterface.TAG_GPS_LONGITUDE; -import static androidx.exifinterface.media.ExifInterface.TAG_GPS_LONGITUDE_REF; -import static androidx.exifinterface.media.ExifInterface.TAG_LENS_MAKE; -import static androidx.exifinterface.media.ExifInterface.TAG_LENS_MODEL; -import static androidx.exifinterface.media.ExifInterface.TAG_LENS_SERIAL_NUMBER; -import static androidx.exifinterface.media.ExifInterface.TAG_LENS_SPECIFICATION; -import static androidx.exifinterface.media.ExifInterface.TAG_MAKE; -import static androidx.exifinterface.media.ExifInterface.TAG_MODEL; -import static androidx.exifinterface.media.ExifInterface.TAG_SOFTWARE; - -/** - * Support utils for EXIF metadata handling - * - */ -public class FileMetadataUtils { - - /** - * Takes EXIF label from sharedPreferences as input and returns relevant EXIF tags - * - * @param pref EXIF sharedPreference label - * @return EXIF tags - */ - public static String[] getTagsFromPref(String pref) { - Timber.d("Retuning tags for pref:%s", pref); - switch (pref) { - case "Author": - return new String[]{TAG_ARTIST, TAG_CAMERA_OWNER_NAME}; - case "Copyright": - return new String[]{TAG_COPYRIGHT}; - case "Location": - return new String[]{TAG_GPS_LATITUDE, TAG_GPS_LATITUDE_REF, - TAG_GPS_LONGITUDE, TAG_GPS_LONGITUDE_REF, - TAG_GPS_ALTITUDE, TAG_GPS_ALTITUDE_REF}; - case "Camera Model": - return new String[]{TAG_MAKE, TAG_MODEL}; - case "Lens Model": - return new String[]{TAG_LENS_MAKE, TAG_LENS_MODEL, TAG_LENS_SPECIFICATION}; - case "Serial Numbers": - return new String[]{TAG_BODY_SERIAL_NUMBER, TAG_LENS_SERIAL_NUMBER}; - case "Software": - return new String[]{TAG_SOFTWARE}; - default: - return new String[]{}; - } - } - -} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/FileMetadataUtils.kt b/app/src/main/java/fr/free/nrw/commons/upload/FileMetadataUtils.kt new file mode 100644 index 000000000..3f27f6f1e --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/upload/FileMetadataUtils.kt @@ -0,0 +1,61 @@ +package fr.free.nrw.commons.upload + +import androidx.exifinterface.media.ExifInterface +import timber.log.Timber + +/** + * Support utils for EXIF metadata handling + * + */ +object FileMetadataUtils { + /** + * Takes EXIF label from sharedPreferences as input and returns relevant EXIF tags + * + * @param pref EXIF sharedPreference label + * @return EXIF tags + */ + fun getTagsFromPref(pref: String): Array { + Timber.d("Retuning tags for pref:%s", pref) + return when (pref) { + "Author" -> arrayOf( + ExifInterface.TAG_ARTIST, + ExifInterface.TAG_CAMERA_OWNER_NAME + ) + + "Copyright" -> arrayOf( + ExifInterface.TAG_COPYRIGHT + ) + + "Location" -> arrayOf( + ExifInterface.TAG_GPS_LATITUDE, + ExifInterface.TAG_GPS_LATITUDE_REF, + ExifInterface.TAG_GPS_LONGITUDE, + ExifInterface.TAG_GPS_LONGITUDE_REF, + ExifInterface.TAG_GPS_ALTITUDE, + ExifInterface.TAG_GPS_ALTITUDE_REF + ) + + "Camera Model" -> arrayOf( + ExifInterface.TAG_MAKE, + ExifInterface.TAG_MODEL + ) + + "Lens Model" -> arrayOf( + ExifInterface.TAG_LENS_MAKE, + ExifInterface.TAG_LENS_MODEL, + ExifInterface.TAG_LENS_SPECIFICATION + ) + + "Serial Numbers" -> arrayOf( + ExifInterface.TAG_BODY_SERIAL_NUMBER, + ExifInterface.TAG_LENS_SERIAL_NUMBER + ) + + "Software" -> arrayOf( + ExifInterface.TAG_SOFTWARE + ) + + else -> arrayOf() + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/FileUtils.java b/app/src/main/java/fr/free/nrw/commons/upload/FileUtils.java deleted file mode 100644 index 8a8fa35b3..000000000 --- a/app/src/main/java/fr/free/nrw/commons/upload/FileUtils.java +++ /dev/null @@ -1,183 +0,0 @@ -package fr.free.nrw.commons.upload; - -import android.content.ContentResolver; -import android.content.Context; -import android.net.Uri; -import android.webkit.MimeTypeMap; - -import androidx.exifinterface.media.ExifInterface; - -import fr.free.nrw.commons.location.LatLng; -import java.io.BufferedReader; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.math.BigInteger; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; - -import java.util.Locale; -import timber.log.Timber; - -public class FileUtils { - - /** - * Get SHA1 of filePath from input stream - */ - static String getSHA1(InputStream is) { - - MessageDigest digest; - try { - digest = MessageDigest.getInstance("SHA1"); - } catch (NoSuchAlgorithmException e) { - Timber.e(e, "Exception while getting Digest"); - return ""; - } - - byte[] buffer = new byte[8192]; - int read; - try { - while ((read = is.read(buffer)) > 0) { - digest.update(buffer, 0, read); - } - byte[] md5sum = digest.digest(); - BigInteger bigInt = new BigInteger(1, md5sum); - String output = bigInt.toString(16); - // Fill to 40 chars - output = String.format("%40s", output).replace(' ', '0'); - Timber.i("File SHA1: %s", output); - - return output; - } catch (IOException e) { - Timber.e(e, "IO Exception"); - return ""; - } finally { - try { - is.close(); - } catch (IOException e) { - Timber.e(e, "Exception on closing MD5 input stream"); - } - } - } - - /** - * Get Geolocation of filePath from input filePath path - */ - static String getGeolocationOfFile(String filePath, LatLng inAppPictureLocation) { - - try { - ExifInterface exifInterface = new ExifInterface(filePath); - ImageCoordinates imageObj = new ImageCoordinates(exifInterface, inAppPictureLocation); - if (imageObj.getDecimalCoords() != null) { // If image has geolocation information in its EXIF - return imageObj.getDecimalCoords(); - } else { - return ""; - } - } catch (IOException e) { - e.printStackTrace(); - return ""; - } - } - - - /** - * Read and return the content of a resource filePath as string. - * - * @param fileName asset filePath's path (e.g. "/queries/radius_query_for_upload_wizard.rq") - * @return the content of the filePath - */ - public static String readFromResource(String fileName) throws IOException { - StringBuilder buffer = new StringBuilder(); - BufferedReader reader = null; - try { - InputStream inputStream = FileUtils.class.getResourceAsStream(fileName); - if (inputStream == null) { - throw new FileNotFoundException(fileName); - } - reader = new BufferedReader(new InputStreamReader(inputStream, "UTF-8")); - String line; - while ((line = reader.readLine()) != null) { - buffer.append(line).append("\n"); - } - } finally { - if (reader != null) { - reader.close(); - } - } - return buffer.toString(); - } - - /** - * Deletes files. - * - * @param file context - */ - public static boolean deleteFile(File file) { - boolean deletedAll = true; - if (file != null) { - if (file.isDirectory()) { - String[] children = file.list(); - for (String child : children) { - deletedAll = deleteFile(new File(file, child)) && deletedAll; - } - } else { - deletedAll = file.delete(); - } - } - - return deletedAll; - } - - public static String getMimeType(Context context, Uri uri) { - String mimeType; - if (uri.getScheme()!=null && uri.getScheme().equals(ContentResolver.SCHEME_CONTENT)) { - ContentResolver cr = context.getContentResolver(); - mimeType = cr.getType(uri); - } else { - String fileExtension = MimeTypeMap.getFileExtensionFromUrl(uri - .toString()); - mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension( - fileExtension.toLowerCase(Locale.getDefault())); - } - return mimeType; - } - - static String getFileExt(String fileName) { - //Default filePath extension - String extension = ".jpg"; - - int i = fileName.lastIndexOf('.'); - if (i > 0) { - extension = fileName.substring(i + 1); - } - return extension; - } - - static FileInputStream getFileInputStream(String filePath) throws FileNotFoundException { - return new FileInputStream(filePath); - } - - public static boolean recursivelyCreateDirs(String dirPath) { - File fileDir = new File(dirPath); - if (!fileDir.exists()) { - return fileDir.mkdirs(); - } - return true; - } - - /** - * Check if file exists in local dirs - */ - public static boolean fileExists(Uri localUri) { - try { - File file = new File(localUri.getPath()); - return file.exists(); - } catch (Exception e) { - Timber.d(e); - return false; - } - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/FileUtils.kt b/app/src/main/java/fr/free/nrw/commons/upload/FileUtils.kt new file mode 100644 index 000000000..1febd8e88 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/upload/FileUtils.kt @@ -0,0 +1,159 @@ +package fr.free.nrw.commons.upload + +import android.content.ContentResolver +import android.content.Context +import android.net.Uri +import android.webkit.MimeTypeMap +import androidx.exifinterface.media.ExifInterface +import fr.free.nrw.commons.location.LatLng +import timber.log.Timber +import java.io.BufferedReader +import java.io.File +import java.io.FileInputStream +import java.io.FileNotFoundException +import java.io.IOException +import java.io.InputStream +import java.io.InputStreamReader +import java.math.BigInteger +import java.security.MessageDigest +import java.security.NoSuchAlgorithmException +import java.util.Locale + +object FileUtils { + + /** + * Get SHA1 of filePath from input stream + */ + fun getSHA1(stream: InputStream): String { + val digest: MessageDigest + try { + digest = MessageDigest.getInstance("SHA1") + } catch (e: NoSuchAlgorithmException) { + Timber.e(e, "Exception while getting Digest") + return "" + } + + val buffer = ByteArray(8192) + var read: Int + try { + while ((stream.read(buffer).also { read = it }) > 0) { + digest.update(buffer, 0, read) + } + val md5sum = digest.digest() + val bigInt = BigInteger(1, md5sum) + var output = bigInt.toString(16) + // Fill to 40 chars + output = String.format("%40s", output).replace(' ', '0') + Timber.i("File SHA1: %s", output) + + return output + } catch (e: IOException) { + Timber.e(e, "IO Exception") + return "" + } finally { + try { + stream.close() + } catch (e: IOException) { + Timber.e(e, "Exception on closing MD5 input stream") + } + } + } + + /** + * Get Geolocation of filePath from input filePath path + */ + fun getGeolocationOfFile(filePath: String, inAppPictureLocation: LatLng?): String? = try { + val exifInterface = ExifInterface(filePath) + val imageObj = ImageCoordinates(exifInterface, inAppPictureLocation) + if (imageObj.decimalCoords != null) { // If image has geolocation information in its EXIF + imageObj.decimalCoords + } else { + "" + } + } catch (e: IOException) { + Timber.e(e) + "" + } + + + /** + * Read and return the content of a resource filePath as string. + * + * @param fileName asset filePath's path (e.g. "/queries/radius_query_for_upload_wizard.rq") + * @return the content of the filePath + */ + @Throws(IOException::class) + fun readFromResource(fileName: String) = buildString { + try { + val inputStream = FileUtils::class.java.getResourceAsStream(fileName) ?: + throw FileNotFoundException(fileName) + + BufferedReader(InputStreamReader(inputStream, "UTF-8")).use { reader -> + var line: String? + while ((reader.readLine().also { line = it }) != null) { + append(line).append("\n") + } + } + } catch (e: Throwable) { + Timber.e(e) + } + } + + /** + * Deletes files. + * + * @param file context + */ + fun deleteFile(file: File?): Boolean { + var deletedAll = true + if (file != null) { + if (file.isDirectory) { + val children = file.list() + for (child in children!!) { + deletedAll = deleteFile(File(file, child)) && deletedAll + } + } else { + deletedAll = file.delete() + } + } + + return deletedAll + } + + fun getMimeType(context: Context, uri: Uri): String? { + val mimeType: String? + if (uri.scheme != null && uri.scheme == ContentResolver.SCHEME_CONTENT) { + val cr = context.contentResolver + mimeType = cr.getType(uri) + } else { + val fileExtension = MimeTypeMap.getFileExtensionFromUrl(uri.toString()) + mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension( + fileExtension.lowercase(Locale.getDefault()) + ) + } + return mimeType + } + + fun getFileExt(fileName: String): String { + //Default filePath extension + var extension = ".jpg" + + val i = fileName.lastIndexOf('.') + if (i > 0) { + extension = fileName.substring(i + 1) + } + return extension + } + + @Throws(FileNotFoundException::class) + fun getFileInputStream(filePath: String?): FileInputStream = + FileInputStream(filePath) + + fun recursivelyCreateDirs(dirPath: String): Boolean { + val fileDir = File(dirPath) + if (!fileDir.exists()) { + return fileDir.mkdirs() + } + return true + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/FileUtilsWrapper.java b/app/src/main/java/fr/free/nrw/commons/upload/FileUtilsWrapper.java deleted file mode 100644 index a35eeebe9..000000000 --- a/app/src/main/java/fr/free/nrw/commons/upload/FileUtilsWrapper.java +++ /dev/null @@ -1,94 +0,0 @@ -package fr.free.nrw.commons.upload; - -import android.content.Context; -import android.net.Uri; -import fr.free.nrw.commons.location.LatLng; -import java.io.BufferedInputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import javax.inject.Inject; -import javax.inject.Singleton; -import timber.log.Timber; - -@Singleton -public class FileUtilsWrapper { - - private final Context context; - - @Inject - public FileUtilsWrapper(final Context context) { - this.context = context; - } - - public String getFileExt(String fileName) { - return FileUtils.getFileExt(fileName); - } - - public String getSHA1(InputStream is) { - return FileUtils.getSHA1(is); - } - - public FileInputStream getFileInputStream(String filePath) throws FileNotFoundException { - return FileUtils.getFileInputStream(filePath); - } - - public String getGeolocationOfFile(String filePath, LatLng inAppPictureLocation) { - return FileUtils.getGeolocationOfFile(filePath, inAppPictureLocation); - } - - public String getMimeType(File file) { - return getMimeType(Uri.parse(file.getPath())); - } - - public String getMimeType(Uri uri) { - return FileUtils.getMimeType(context, uri); - } - - /** - * Takes a file as input and returns an Observable of files with the specified chunk size - */ - public List getFileChunks(File file, final int chunkSize) - throws IOException { - final byte[] buffer = new byte[chunkSize]; - - //try-with-resources to ensure closing stream - try (final FileInputStream fis = new FileInputStream(file); - final BufferedInputStream bis = new BufferedInputStream(fis)) { - final List buffers = new ArrayList<>(); - int size; - while ((size = bis.read(buffer)) > 0) { - buffers.add(writeToFile(Arrays.copyOf(buffer, size), file.getName(), - getFileExt(file.getName()))); - } - return buffers; - } - } - - /** - * Create a temp file containing the passed byte data. - */ - private File writeToFile(final byte[] data, final String fileName, - String fileExtension) - throws IOException { - final File file = File.createTempFile(fileName, fileExtension, context.getCacheDir()); - try { - if (!file.exists()) { - file.createNewFile(); - } - final FileOutputStream fos = new FileOutputStream(file); - fos.write(data); - fos.close(); - } catch (final Exception throwable) { - Timber.e(throwable, "Failed to create file"); - } - return file; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/FileUtilsWrapper.kt b/app/src/main/java/fr/free/nrw/commons/upload/FileUtilsWrapper.kt new file mode 100644 index 000000000..aa1f8aed6 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/upload/FileUtilsWrapper.kt @@ -0,0 +1,85 @@ +package fr.free.nrw.commons.upload + +import android.content.Context +import android.net.Uri +import fr.free.nrw.commons.location.LatLng +import fr.free.nrw.commons.upload.FileUtils.getMimeType +import timber.log.Timber +import java.io.BufferedInputStream +import java.io.File +import java.io.FileInputStream +import java.io.FileNotFoundException +import java.io.FileOutputStream +import java.io.IOException +import java.io.InputStream +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class FileUtilsWrapper @Inject constructor(private val context: Context) { + fun getSHA1(stream: InputStream?): String = + stream?.let { FileUtils.getSHA1(it) } ?: "" + + @Throws(FileNotFoundException::class) + fun getFileInputStream(filePath: String?): FileInputStream = + FileUtils.getFileInputStream(filePath) + + fun getGeolocationOfFile(filePath: String, inAppPictureLocation: LatLng?): String? = + FileUtils.getGeolocationOfFile(filePath, inAppPictureLocation) + + fun getMimeType(file: File?): String? = + getMimeType(Uri.parse(file?.path)) + + fun getMimeType(uri: Uri): String? = + getMimeType(context, uri) + + /** + * Takes a file as input and returns an Observable of files with the specified chunk size + */ + @Throws(IOException::class) + fun getFileChunks(file: File?, chunkSize: Int): List { + if (file == null) return emptyList() + + val buffer = ByteArray(chunkSize) + + FileInputStream(file).use { fis -> + BufferedInputStream(fis).use { bis -> + val buffers: MutableList = ArrayList() + var size: Int + while ((bis.read(buffer).also { size = it }) > 0) { + buffers.add( + writeToFile( + buffer.copyOf(size), + file.name ?: "", + getFileExt(file.name) + ) + ) + } + return buffers + } + } + } + + private fun getFileExt(fileName: String): String = + FileUtils.getFileExt(fileName) + + /** + * Create a temp file containing the passed byte data. + */ + @Throws(IOException::class) + private fun writeToFile(data: ByteArray, fileName: String, fileExtension: String): File { + val file = File.createTempFile(fileName, fileExtension, context.cacheDir) + try { + if (!file.exists()) { + file.createNewFile() + } + + FileOutputStream(file).use { fos -> + fos.write(data) + } + } catch (throwable: Exception) { + Timber.e(throwable, "Failed to create file") + } + return file + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/ImageProcessingService.java b/app/src/main/java/fr/free/nrw/commons/upload/ImageProcessingService.java deleted file mode 100644 index 8065fde56..000000000 --- a/app/src/main/java/fr/free/nrw/commons/upload/ImageProcessingService.java +++ /dev/null @@ -1,190 +0,0 @@ -package fr.free.nrw.commons.upload; - -import static fr.free.nrw.commons.utils.ImageUtils.EMPTY_CAPTION; -import static fr.free.nrw.commons.utils.ImageUtils.FILE_NAME_EXISTS; -import static fr.free.nrw.commons.utils.ImageUtils.IMAGE_DUPLICATE; -import static fr.free.nrw.commons.utils.ImageUtils.IMAGE_KEEP; -import static fr.free.nrw.commons.utils.ImageUtils.IMAGE_OK; - -import android.content.Context; -import fr.free.nrw.commons.location.LatLng; -import fr.free.nrw.commons.media.MediaClient; -import fr.free.nrw.commons.nearby.Place; -import fr.free.nrw.commons.utils.ImageUtilsWrapper; -import io.reactivex.Single; -import io.reactivex.schedulers.Schedulers; -import java.util.List; -import javax.inject.Inject; -import javax.inject.Singleton; -import org.apache.commons.lang3.StringUtils; -import timber.log.Timber; - -/** - * Methods for pre-processing images to be uploaded - */ -@Singleton -public class ImageProcessingService { - - private final FileUtilsWrapper fileUtilsWrapper; - private final ImageUtilsWrapper imageUtilsWrapper; - private final ReadFBMD readFBMD; - private final EXIFReader EXIFReader; - private final MediaClient mediaClient; - - @Inject - public ImageProcessingService(FileUtilsWrapper fileUtilsWrapper, - ImageUtilsWrapper imageUtilsWrapper, - ReadFBMD readFBMD, EXIFReader EXIFReader, - MediaClient mediaClient, Context context) { - this.fileUtilsWrapper = fileUtilsWrapper; - this.imageUtilsWrapper = imageUtilsWrapper; - this.readFBMD = readFBMD; - this.EXIFReader = EXIFReader; - this.mediaClient = mediaClient; - } - - - /** - * Check image quality before upload - checks duplicate image - checks dark image - checks - * geolocation for image - * - * @param uploadItem UploadItem whose quality is to be checked - * @param inAppPictureLocation In app picture location (if any) - * @return Quality of UploadItem - */ - Single validateImage(UploadItem uploadItem, LatLng inAppPictureLocation) { - int currentImageQuality = uploadItem.getImageQuality(); - Timber.d("Current image quality is %d", currentImageQuality); - if (currentImageQuality == IMAGE_KEEP || currentImageQuality == IMAGE_OK) { - return Single.just(IMAGE_OK); - } - Timber.d("Checking the validity of image"); - String filePath = uploadItem.getMediaUri().getPath(); - - return Single.zip( - checkDuplicateImage(filePath), - checkImageGeoLocation(uploadItem.getPlace(), filePath, inAppPictureLocation), - checkDarkImage(filePath), - checkFBMD(filePath), - checkEXIF(filePath), - (duplicateImage, wrongGeoLocation, darkImage, fbmd, exif) -> { - Timber.d("duplicate: %d, geo: %d, dark: %d" + "fbmd:" + fbmd + "exif:" - + exif, - duplicateImage, wrongGeoLocation, darkImage); - return duplicateImage | wrongGeoLocation | darkImage | fbmd | exif; - } - ); - } - - /** - * Checks caption of the given UploadItem - * - * @param uploadItem UploadItem whose caption is to be verified - * @return Quality of caption of the UploadItem - */ - Single validateCaption(UploadItem uploadItem) { - int currentImageQuality = uploadItem.getImageQuality(); - Timber.d("Current image quality is %d", currentImageQuality); - if (currentImageQuality == IMAGE_KEEP) { - return Single.just(IMAGE_OK); - } - Timber.d("Checking the validity of caption"); - - return validateItemTitle(uploadItem); - } - - /** - * We want to discourage users from uploading images to Commons that were taken from Facebook. - * This attempts to detect whether an image was downloaded from Facebook by heuristically - * searching for metadata that is specific to images that come from Facebook. - */ - private Single checkFBMD(String filepath) { - return readFBMD.processMetadata(filepath); - } - - /** - * We try to minimize uploads from the Commons app that might be copyright violations. If an - * image does not have any Exif metadata, then it was likely downloaded from the internet, and - * is probably not an original work by the user. We detect these kinds of images by looking for - * the presence of some basic Exif metadata. - */ - private Single checkEXIF(String filepath) { - return EXIFReader.processMetadata(filepath); - } - - - /** - * Checks item caption - empty caption - existing caption - * - * @param uploadItem - * @return - */ - private Single validateItemTitle(UploadItem uploadItem) { - Timber.d("Checking for image title %s", uploadItem.getUploadMediaDetails()); - List captions = uploadItem.getUploadMediaDetails(); - if (captions.isEmpty()) { - return Single.just(EMPTY_CAPTION); - } - - return mediaClient.checkPageExistsUsingTitle("File:" + uploadItem.getFileName()) - .map(doesFileExist -> { - Timber.d("Result for valid title is %s", doesFileExist); - return doesFileExist ? FILE_NAME_EXISTS : IMAGE_OK; - }) - .subscribeOn(Schedulers.io()); - } - - /** - * Checks for duplicate image - * - * @param filePath file to be checked - * @return IMAGE_DUPLICATE or IMAGE_OK - */ - Single checkDuplicateImage(String filePath) { - Timber.d("Checking for duplicate image %s", filePath); - return Single.fromCallable(() -> fileUtilsWrapper.getFileInputStream(filePath)) - .map(fileUtilsWrapper::getSHA1) - .flatMap(mediaClient::checkFileExistsUsingSha) - .map(b -> { - Timber.d("Result for duplicate image %s", b); - return b ? IMAGE_DUPLICATE : IMAGE_OK; - }) - .subscribeOn(Schedulers.io()); - } - - /** - * Checks for dark image - * - * @param filePath file to be checked - * @return IMAGE_DARK or IMAGE_OK - */ - private Single checkDarkImage(String filePath) { - Timber.d("Checking for dark image %s", filePath); - return imageUtilsWrapper.checkIfImageIsTooDark(filePath); - } - - /** - * Checks for image geolocation returns IMAGE_OK if the place is null or if the file doesn't - * contain a geolocation - * - * @param filePath file to be checked - * @return IMAGE_GEOLOCATION_DIFFERENT or IMAGE_OK - */ - private Single checkImageGeoLocation(Place place, String filePath, LatLng inAppPictureLocation) { - Timber.d("Checking for image geolocation %s", filePath); - if (place == null || StringUtils.isBlank(place.getWikiDataEntityId())) { - return Single.just(IMAGE_OK); - } - return Single.fromCallable(() -> filePath) - .flatMap(path -> Single.just(fileUtilsWrapper.getGeolocationOfFile(path, inAppPictureLocation))) - .flatMap(geoLocation -> { - if (StringUtils.isBlank(geoLocation)) { - return Single.just(IMAGE_OK); - } - return imageUtilsWrapper - .checkImageGeolocationIsDifferent(geoLocation, place.getLocation()); - }) - .subscribeOn(Schedulers.io()); - } -} - diff --git a/app/src/main/java/fr/free/nrw/commons/upload/ImageProcessingService.kt b/app/src/main/java/fr/free/nrw/commons/upload/ImageProcessingService.kt new file mode 100644 index 000000000..9fbb1f1e4 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/upload/ImageProcessingService.kt @@ -0,0 +1,183 @@ +package fr.free.nrw.commons.upload + +import fr.free.nrw.commons.location.LatLng +import fr.free.nrw.commons.media.MediaClient +import fr.free.nrw.commons.nearby.Place +import fr.free.nrw.commons.utils.ImageUtils.EMPTY_CAPTION +import fr.free.nrw.commons.utils.ImageUtils.FILE_NAME_EXISTS +import fr.free.nrw.commons.utils.ImageUtils.IMAGE_DUPLICATE +import fr.free.nrw.commons.utils.ImageUtils.IMAGE_KEEP +import fr.free.nrw.commons.utils.ImageUtils.IMAGE_OK +import fr.free.nrw.commons.utils.ImageUtilsWrapper +import io.reactivex.Single +import io.reactivex.functions.Function +import io.reactivex.schedulers.Schedulers +import org.apache.commons.lang3.StringUtils +import timber.log.Timber +import java.io.FileInputStream +import javax.inject.Inject +import javax.inject.Singleton + +/** + * Methods for pre-processing images to be uploaded + */ +@Singleton +class ImageProcessingService @Inject constructor( + private val fileUtilsWrapper: FileUtilsWrapper, + private val imageUtilsWrapper: ImageUtilsWrapper, + private val readFBMD: ReadFBMD, + private val EXIFReader: EXIFReader, + private val mediaClient: MediaClient +) { + /** + * Check image quality before upload - checks duplicate image - checks dark image - checks + * geolocation for image + * + * @param uploadItem UploadItem whose quality is to be checked + * @param inAppPictureLocation In app picture location (if any) + * @return Quality of UploadItem + */ + fun validateImage(uploadItem: UploadItem, inAppPictureLocation: LatLng?): Single { + val currentImageQuality = uploadItem.imageQuality + Timber.d("Current image quality is %d", currentImageQuality) + if (currentImageQuality == IMAGE_KEEP || currentImageQuality == IMAGE_OK) { + return Single.just(IMAGE_OK) + } + + Timber.d("Checking the validity of image") + val filePath = uploadItem.mediaUri.path + + return Single.zip( + checkDuplicateImage(filePath), + checkImageGeoLocation(uploadItem.place, filePath, inAppPictureLocation), + checkDarkImage(filePath!!), + checkFBMD(filePath), + checkEXIF(filePath) + ) { duplicateImage: Int, wrongGeoLocation: Int, darkImage: Int, fbmd: Int, exif: Int -> + Timber.d( + "duplicate: %d, geo: %d, dark: %d, fbmd: %d, exif: %d", + duplicateImage, wrongGeoLocation, darkImage, fbmd, exif + ) + return@zip duplicateImage or wrongGeoLocation or darkImage or fbmd or exif + } + } + + /** + * Checks caption of the given UploadItem + * + * @param uploadItem UploadItem whose caption is to be verified + * @return Quality of caption of the UploadItem + */ + fun validateCaption(uploadItem: UploadItem): Single { + val currentImageQuality = uploadItem.imageQuality + Timber.d("Current image quality is %d", currentImageQuality) + if (currentImageQuality == IMAGE_KEEP) { + return Single.just(IMAGE_OK) + } + Timber.d("Checking the validity of caption") + + return validateItemTitle(uploadItem) + } + + /** + * We want to discourage users from uploading images to Commons that were taken from Facebook. + * This attempts to detect whether an image was downloaded from Facebook by heuristically + * searching for metadata that is specific to images that come from Facebook. + */ + private fun checkFBMD(filepath: String?): Single = + readFBMD.processMetadata(filepath) + + /** + * We try to minimize uploads from the Commons app that might be copyright violations. If an + * image does not have any Exif metadata, then it was likely downloaded from the internet, and + * is probably not an original work by the user. We detect these kinds of images by looking for + * the presence of some basic Exif metadata. + */ + private fun checkEXIF(filepath: String): Single = + EXIFReader.processMetadata(filepath) + + + /** + * Checks item caption - empty caption - existing caption + */ + private fun validateItemTitle(uploadItem: UploadItem): Single { + Timber.d("Checking for image title %s", uploadItem.uploadMediaDetails) + val captions = uploadItem.uploadMediaDetails + if (captions.isEmpty()) { + return Single.just(EMPTY_CAPTION) + } + + return mediaClient.checkPageExistsUsingTitle("File:" + uploadItem.fileName) + .map { doesFileExist: Boolean -> + Timber.d("Result for valid title is %s", doesFileExist) + if (doesFileExist) FILE_NAME_EXISTS else IMAGE_OK + } + .subscribeOn(Schedulers.io()) + } + + /** + * Checks for duplicate image + * + * @param filePath file to be checked + * @return IMAGE_DUPLICATE or IMAGE_OK + */ + fun checkDuplicateImage(filePath: String?): Single { + Timber.d("Checking for duplicate image %s", filePath) + return Single.fromCallable { fileUtilsWrapper.getFileInputStream(filePath) } + .map { stream: FileInputStream? -> + fileUtilsWrapper.getSHA1(stream) + } + .flatMap { fileSha: String? -> + mediaClient.checkFileExistsUsingSha(fileSha) + } + .map { + Timber.d("Result for duplicate image %s", it) + if (it) IMAGE_DUPLICATE else IMAGE_OK + } + .subscribeOn(Schedulers.io()) + } + + /** + * Checks for dark image + * + * @param filePath file to be checked + * @return IMAGE_DARK or IMAGE_OK + */ + private fun checkDarkImage(filePath: String): Single { + Timber.d("Checking for dark image %s", filePath) + return imageUtilsWrapper.checkIfImageIsTooDark(filePath) + } + + /** + * Checks for image geolocation returns IMAGE_OK if the place is null or if the file doesn't + * contain a geolocation + * + * @param filePath file to be checked + * @return IMAGE_GEOLOCATION_DIFFERENT or IMAGE_OK + */ + private fun checkImageGeoLocation( + place: Place?, + filePath: String?, + inAppPictureLocation: LatLng? + ): Single { + Timber.d("Checking for image geolocation %s", filePath) + if (place == null || StringUtils.isBlank(place.wikiDataEntityId)) { + return Single.just(IMAGE_OK) + } + + return Single.fromCallable { filePath } + .flatMap { path: String? -> + Single.just( + fileUtilsWrapper.getGeolocationOfFile(path!!, inAppPictureLocation) + ) + } + .flatMap { geoLocation: String? -> + if (geoLocation.isNullOrBlank()) { + return@flatMap Single.just(IMAGE_OK) + } + imageUtilsWrapper.checkImageGeolocationIsDifferent(geoLocation, place.getLocation()) + } + .subscribeOn(Schedulers.io()) + } +} + diff --git a/app/src/main/java/fr/free/nrw/commons/upload/PageContentsCreator.java b/app/src/main/java/fr/free/nrw/commons/upload/PageContentsCreator.java deleted file mode 100644 index fb71da3c7..000000000 --- a/app/src/main/java/fr/free/nrw/commons/upload/PageContentsCreator.java +++ /dev/null @@ -1,127 +0,0 @@ -package fr.free.nrw.commons.upload; - -import android.content.Context; -import androidx.annotation.NonNull; -import fr.free.nrw.commons.Media; -import fr.free.nrw.commons.Utils; -import fr.free.nrw.commons.contributions.Contribution; -import fr.free.nrw.commons.filepicker.UploadableFile.DateTimeWithSource; -import fr.free.nrw.commons.settings.Prefs.Licenses; -import fr.free.nrw.commons.utils.ConfigUtils; -import java.text.SimpleDateFormat; -import java.util.Calendar; -import java.util.Date; -import java.util.List; -import java.util.Locale; -import javax.inject.Inject; -import org.apache.commons.lang3.StringUtils; -import timber.log.Timber; - -public class PageContentsCreator { - - //{{According to Exif data|2009-01-09}} - private static final String TEMPLATE_DATE_ACC_TO_EXIF = "{{According to Exif data|%s}}"; - - //2009-01-09 → 9 January 2009 - private static final String TEMPLATE_DATA_OTHER_SOURCE = "%s"; - - private final Context context; - - @Inject - public PageContentsCreator(final Context context) { - this.context = context; - } - - public String createFrom(final Contribution contribution) { - StringBuilder buffer = new StringBuilder(); - final Media media = contribution.getMedia(); - buffer - .append("== {{int:filedesc}} ==\n") - .append("{{Information\n") - .append("|description=").append(media.getFallbackDescription()).append("\n"); - if (contribution.getWikidataPlace() != null) { - buffer.append("{{ on Wikidata|").append(contribution.getWikidataPlace().getId()) - .append("}}"); - } - buffer - .append("|source=").append("{{own}}\n") - .append("|author=[[User:").append(media.getAuthor()).append("|") - .append(media.getAuthor()).append("]]\n"); - - final String templatizedCreatedDate = getTemplatizedCreatedDate( - contribution.getDateCreatedString(), contribution.getDateCreated(), contribution.getDateCreatedSource()); - if (!StringUtils.isBlank(templatizedCreatedDate)) { - buffer.append("|date=").append(templatizedCreatedDate); - } - - buffer.append("}}").append("\n"); - - //Only add Location template (e.g. {{Location|37.51136|-77.602615}} ) if coords is not null - final String decimalCoords = contribution.getDecimalCoords(); - if (decimalCoords != null) { - buffer.append("{{Location|").append(decimalCoords).append("}}").append("\n"); - } - - if (contribution.getWikidataPlace() != null && contribution.getWikidataPlace().isMonumentUpload()) { - buffer.append(String.format(Locale.ENGLISH, "{{Wiki Loves Monuments %d|1= %s}}\n", - Utils.getWikiLovesMonumentsYear(Calendar.getInstance()), contribution.getCountryCode())); - } - - buffer - .append("\n") - .append("== {{int:license-header}} ==\n") - .append(licenseTemplateFor(media.getLicense())).append("\n\n") - .append("{{Uploaded from Mobile|platform=Android|version=") - .append(ConfigUtils.getVersionNameWithSha(context)).append("}}\n"); - final List categories = media.getCategories(); - if (categories != null && categories.size() != 0) { - for (int i = 0; i < categories.size(); i++) { - buffer.append("\n[[Category:").append(categories.get(i)).append("]]"); - } - } else { - buffer.append("{{subst:unc}}"); - } - Timber.d("Template: %s", buffer.toString()); - return buffer.toString(); - } - - /** - * Returns upload date in either TEMPLATE_DATE_ACC_TO_EXIF or TEMPLATE_DATA_OTHER_SOURCE - * - * @param dateCreated - * @param dateCreatedSource - * @return - */ - private String getTemplatizedCreatedDate(String dateCreatedString, Date dateCreated, String dateCreatedSource) { - if (dateCreated != null) { - SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); - return String.format(Locale.ENGLISH, - isExif(dateCreatedSource) ? TEMPLATE_DATE_ACC_TO_EXIF : TEMPLATE_DATA_OTHER_SOURCE, - isExif(dateCreatedSource) ? dateCreatedString: dateFormat.format(dateCreated) - ) + "\n"; - } - return ""; - } - - private boolean isExif(String dateCreatedSource) { - return DateTimeWithSource.EXIF_SOURCE.equals(dateCreatedSource); - } - - @NonNull - private String licenseTemplateFor(String license) { - switch (license) { - case Licenses.CC_BY_3: - return "{{self|cc-by-3.0}}"; - case Licenses.CC_BY_4: - return "{{self|cc-by-4.0}}"; - case Licenses.CC_BY_SA_3: - return "{{self|cc-by-sa-3.0}}"; - case Licenses.CC_BY_SA_4: - return "{{self|cc-by-sa-4.0}}"; - case Licenses.CC0: - return "{{self|cc-zero}}"; - } - - throw new RuntimeException("Unrecognized license value: " + license); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/PageContentsCreator.kt b/app/src/main/java/fr/free/nrw/commons/upload/PageContentsCreator.kt new file mode 100644 index 000000000..0c4ded8b2 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/upload/PageContentsCreator.kt @@ -0,0 +1,106 @@ +package fr.free.nrw.commons.upload + +import android.content.Context +import fr.free.nrw.commons.Utils +import fr.free.nrw.commons.contributions.Contribution +import fr.free.nrw.commons.filepicker.UploadableFile.DateTimeWithSource +import fr.free.nrw.commons.settings.Prefs.Licenses +import fr.free.nrw.commons.utils.ConfigUtils.getVersionNameWithSha +import org.apache.commons.lang3.StringUtils +import java.text.SimpleDateFormat +import java.util.Calendar +import java.util.Date +import java.util.Locale +import javax.inject.Inject + +class PageContentsCreator @Inject constructor(private val context: Context) { + fun createFrom(contribution: Contribution?): String = buildString { + val media = contribution?.media + append("== {{int:filedesc}} ==\n") + append("{{Information\n") + append("|description=").append(media?.fallbackDescription).append("\n") + if (contribution?.wikidataPlace != null) { + append("{{ on Wikidata|").append(contribution.wikidataPlace!!.id) + append("}}") + } + append("|source=").append("{{own}}\n") + append("|author=[[User:").append(media?.author).append("|") + append(media?.author).append("]]\n") + + val templatizedCreatedDate = getTemplatizedCreatedDate( + contribution?.dateCreatedString, + contribution?.dateCreated, + contribution?.dateCreatedSource + ) + if (!StringUtils.isBlank(templatizedCreatedDate)) { + append("|date=").append(templatizedCreatedDate) + } + + append("}}").append("\n") + + //Only add Location template (e.g. {{Location|37.51136|-77.602615}} ) if coords is not null + val decimalCoords = contribution?.decimalCoords + if (decimalCoords != null) { + append("{{Location|").append(decimalCoords).append("}}").append("\n") + } + + if (contribution?.wikidataPlace != null && contribution.wikidataPlace!!.isMonumentUpload) { + append( + String.format( + Locale.ENGLISH, + "{{Wiki Loves Monuments %d|1= %s}}\n", + Utils.getWikiLovesMonumentsYear(Calendar.getInstance()), + contribution.countryCode + ) + ) + } + + append("\n") + append("== {{int:license-header}} ==\n") + append(licenseTemplateFor(media?.license!!)).append("\n\n") + append("{{Uploaded from Mobile|platform=Android|version=") + append(context.getVersionNameWithSha()).append("}}\n") + val categories = media.categories + if (!categories.isNullOrEmpty()) { + categories.indices.forEach { + append("\n[[Category:").append(categories[it]).append("]]") + } + } else { + append("{{subst:unc}}") + } + } + + /** + * Returns upload date in either TEMPLATE_DATE_ACC_TO_EXIF or TEMPLATE_DATA_OTHER_SOURCE + */ + private fun getTemplatizedCreatedDate( + dateCreatedString: String?, dateCreated: Date?, dateCreatedSource: String? + ) = dateCreated?.let { + val dateFormat = SimpleDateFormat("yyyy-MM-dd") + String.format( + Locale.ENGLISH, + if (isExif(dateCreatedSource)) TEMPLATE_DATE_ACC_TO_EXIF else TEMPLATE_DATA_OTHER_SOURCE, + if (isExif(dateCreatedSource)) dateCreatedString else dateFormat.format(dateCreated) + ) + "\n" + } ?: "" + + private fun isExif(dateCreatedSource: String?): Boolean = + DateTimeWithSource.EXIF_SOURCE == dateCreatedSource + + private fun licenseTemplateFor(license: String) = when (license) { + Licenses.CC_BY_3 -> "{{self|cc-by-3.0}}" + Licenses.CC_BY_4 -> "{{self|cc-by-4.0}}" + Licenses.CC_BY_SA_3 -> "{{self|cc-by-sa-3.0}}" + Licenses.CC_BY_SA_4 -> "{{self|cc-by-sa-4.0}}" + Licenses.CC0 -> "{{self|cc-zero}}" + else -> throw RuntimeException("Unrecognized license value: $license") + } + + companion object { + //{{According to Exif data|2009-01-09}} + private const val TEMPLATE_DATE_ACC_TO_EXIF = "{{According to Exif data|%s}}" + + //2009-01-09 → 9 January 2009 + private const val TEMPLATE_DATA_OTHER_SOURCE = "%s" + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/PendingUploadsContract.java b/app/src/main/java/fr/free/nrw/commons/upload/PendingUploadsContract.java deleted file mode 100644 index 8b86ecbd2..000000000 --- a/app/src/main/java/fr/free/nrw/commons/upload/PendingUploadsContract.java +++ /dev/null @@ -1,31 +0,0 @@ -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 { - - /** - * Deletes a upload. - */ - void deleteUpload(Contribution contribution, Context context); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/PendingUploadsContract.kt b/app/src/main/java/fr/free/nrw/commons/upload/PendingUploadsContract.kt new file mode 100644 index 000000000..6c18ba622 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/upload/PendingUploadsContract.kt @@ -0,0 +1,26 @@ +package fr.free.nrw.commons.upload + +import android.content.Context +import fr.free.nrw.commons.BasePresenter +import fr.free.nrw.commons.contributions.Contribution + +/** + * The contract using which the PendingUploadsFragment or FailedUploadsFragment would communicate + * with its PendingUploadsPresenter + */ +class PendingUploadsContract { + /** + * Interface representing the view for uploads. + */ + interface View + + /** + * Interface representing the user actions related to uploads. + */ + interface UserActionListener : BasePresenter { + /** + * Deletes a upload. + */ + fun deleteUpload(contribution: Contribution?, context: Context?) + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/PendingUploadsFragment.kt b/app/src/main/java/fr/free/nrw/commons/upload/PendingUploadsFragment.kt index 4442a64ea..45e9d90a3 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/PendingUploadsFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/PendingUploadsFragment.kt @@ -10,6 +10,9 @@ import androidx.recyclerview.widget.LinearLayoutManager import fr.free.nrw.commons.CommonsApplication import fr.free.nrw.commons.R import fr.free.nrw.commons.contributions.Contribution +import fr.free.nrw.commons.contributions.Contribution.Companion.STATE_IN_PROGRESS +import fr.free.nrw.commons.contributions.Contribution.Companion.STATE_PAUSED +import fr.free.nrw.commons.contributions.Contribution.Companion.STATE_QUEUED import fr.free.nrw.commons.databinding.FragmentPendingUploadsBinding import fr.free.nrw.commons.di.CommonsDaggerSupportFragment import fr.free.nrw.commons.utils.DialogUtil.showAlertDialog @@ -35,7 +38,8 @@ class PendingUploadsFragment : private lateinit var adapter: PendingUploadsAdapter private var contributionsSize = 0 - var contributionsList = ArrayList() + + private var contributionsList = mutableListOf() override fun onAttach(context: Context) { super.onAttach(context) @@ -48,7 +52,7 @@ class PendingUploadsFragment : inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?, - ): View? { + ): View { super.onCreate(savedInstanceState) binding = FragmentPendingUploadsBinding.inflate(inflater, container, false) pendingUploadsPresenter.onAttachView(this) @@ -71,27 +75,24 @@ class PendingUploadsFragment : /** * Initializes the recycler view. */ - fun initRecyclerView() { + private fun initRecyclerView() { binding.pendingUploadsRecyclerView.setLayoutManager(LinearLayoutManager(this.context)) binding.pendingUploadsRecyclerView.adapter = adapter pendingUploadsPresenter.setup() - pendingUploadsPresenter.totalContributionList.observe( - viewLifecycleOwner, - ) { list: PagedList -> + pendingUploadsPresenter.totalContributionList + .observe(viewLifecycleOwner) { list: PagedList -> contributionsSize = list.size - contributionsList = ArrayList() + contributionsList = mutableListOf() 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 + if (it.state == STATE_PAUSED || + it.state == STATE_QUEUED || + it.state == STATE_IN_PROGRESS ) { contributionsList.add(it) } - if (it.state == Contribution.STATE_PAUSED || - it.state == Contribution.STATE_QUEUED - ) { + if (it.state == STATE_PAUSED || it.state == STATE_QUEUED) { pausedOrQueuedUploads++ } } @@ -104,7 +105,7 @@ class PendingUploadsFragment : binding.nopendingTextView.visibility = View.GONE binding.pendingUplaodsLl.visibility = View.VISIBLE adapter.submitList(list) - binding.progressTextView.setText(contributionsSize.toString() + " uploads left") + binding.progressTextView.setText("$contributionsSize uploads left") if ((pausedOrQueuedUploads == contributionsSize) || CommonsApplication.isPaused) { uploadProgressActivity.setPausedIcon(true) } else { @@ -118,23 +119,18 @@ class PendingUploadsFragment : * Cancels a specific upload after getting a confirmation from the user using Dialog. */ override fun deleteUpload(contribution: Contribution?) { + val activity = requireActivity() + val locale = Locale.getDefault() 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)), + activity, + String.format(locale, activity.getString(R.string.cancelling_upload)), + String.format(locale, activity.getString(R.string.cancel_upload_dialog)), + String.format(locale, activity.getString(R.string.yes)), + String.format(locale, activity.getString(R.string.no)), { ViewUtil.showShortToast(context, R.string.cancelling_upload) pendingUploadsPresenter.deleteUpload( - contribution, - this.requireContext().applicationContext, + contribution, requireContext().applicationContext, ) }, {}, @@ -144,47 +140,35 @@ class PendingUploadsFragment : /** * Restarts all the paused uploads. */ - fun restartUploads() { - if (contributionsList != null) { - pendingUploadsPresenter.restartUploads( - contributionsList, - 0, - this.requireContext().applicationContext, - ) - } - } + fun restartUploads() = pendingUploadsPresenter.restartUploads( + contributionsList, 0, requireContext().applicationContext + ) /** * Pauses all the ongoing uploads. */ - fun pauseUploads() { - pendingUploadsPresenter.pauseUploads() - } + fun pauseUploads() = pendingUploadsPresenter.pauseUploads() /** * Cancels all the uploads after getting a confirmation from the user using Dialog. */ fun deleteUploads() { + val activity = requireActivity() + val locale = Locale.getDefault() 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)), + activity, + String.format(locale, activity.getString(R.string.cancelling_all_the_uploads)), + String.format(locale, activity.getString(R.string.are_you_sure_that_you_want_cancel_all_the_uploads)), + String.format(locale, activity.getString(R.string.yes)), + String.format(locale, activity.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, + STATE_QUEUED, + STATE_IN_PROGRESS, + STATE_PAUSED, ), ) }, diff --git a/app/src/main/java/fr/free/nrw/commons/upload/PendingUploadsPresenter.java b/app/src/main/java/fr/free/nrw/commons/upload/PendingUploadsPresenter.java deleted file mode 100644 index ecc9c19b5..000000000 --- a/app/src/main/java/fr/free/nrw/commons/upload/PendingUploadsPresenter.java +++ /dev/null @@ -1,263 +0,0 @@ -package fr.free.nrw.commons.upload; - - -import static fr.free.nrw.commons.di.CommonsApplicationModule.IO_THREAD; -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> totalContributionList; - LiveData> failedContributionList; - - @Inject - PendingUploadsPresenter( - final ContributionBoundaryCallback contributionBoundaryCallback, - final ContributionsRemoteDataSource contributionsRemoteDataSource, - final ContributionsRepository contributionsRepository, - final UploadRepository uploadRepository, - @Named(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 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 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 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 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 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))); - } - } - -} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/PendingUploadsPresenter.kt b/app/src/main/java/fr/free/nrw/commons/upload/PendingUploadsPresenter.kt new file mode 100644 index 000000000..324f988d4 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/upload/PendingUploadsPresenter.kt @@ -0,0 +1,256 @@ +package fr.free.nrw.commons.upload + +import android.content.Context +import androidx.lifecycle.LiveData +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.Contribution.Companion.STATE_FAILED +import fr.free.nrw.commons.contributions.Contribution.Companion.STATE_IN_PROGRESS +import fr.free.nrw.commons.contributions.Contribution.Companion.STATE_PAUSED +import fr.free.nrw.commons.contributions.Contribution.Companion.STATE_QUEUED +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.worker.WorkRequestHelper.Companion.makeOneTimeWorkRequest +import fr.free.nrw.commons.utils.ImageUtils.IMAGE_OK +import io.reactivex.Scheduler +import io.reactivex.disposables.CompositeDisposable +import timber.log.Timber +import java.util.Calendar +import javax.inject.Inject +import javax.inject.Named + + +/** + * The presenter class for PendingUploadsFragment and FailedUploadsFragment + */ +class PendingUploadsPresenter @Inject internal constructor( + private val contributionBoundaryCallback: ContributionBoundaryCallback, + private val contributionsRemoteDataSource: ContributionsRemoteDataSource, + private val contributionsRepository: ContributionsRepository, + private val uploadRepository: UploadRepository, + @param:Named(CommonsApplicationModule.IO_THREAD) private val ioThreadScheduler: Scheduler +) : PendingUploadsContract.UserActionListener { + private val compositeDisposable = CompositeDisposable() + + lateinit var totalContributionList: LiveData> + lateinit var failedContributionList: LiveData> + + /** + * 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 + */ + fun setup() { + val pagedListConfig = PagedList.Config.Builder() + .setPrefetchDistance(50) + .setPageSize(10).build() + + val factory = contributionsRepository.fetchContributionsWithStatesSortedByDateUploadStarted( + listOf(STATE_QUEUED, STATE_IN_PROGRESS, STATE_PAUSED) + ) + totalContributionList = LivePagedListBuilder(factory, pagedListConfig).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 + */ + fun getFailedContributions() { + val pagedListConfig = PagedList.Config.Builder() + .setPrefetchDistance(50) + .setPageSize(10).build() + + val factory = contributionsRepository.fetchContributionsWithStatesSortedByDateUploadStarted( + listOf(STATE_FAILED) + ) + failedContributionList = LivePagedListBuilder(factory, pagedListConfig).build() + } + + override fun onAttachView(view: PendingUploadsContract.View) { + } + + override fun 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 fun deleteUpload(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. + */ + fun pauseUploads() { + CommonsApplication.isPaused = true + compositeDisposable.add( + contributionsRepository + .updateContributionsWithStates( + listOf(STATE_QUEUED, STATE_IN_PROGRESS), + 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. + */ + fun deleteUploads(states: List) { + 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. + */ + fun restartUploads(contributionList: List, index: Int, context: Context) { + CommonsApplication.isPaused = false + if (index >= contributionList.size) { + return + } + val contribution = contributionList[index] + if (contribution.state == STATE_FAILED) { + contribution.dateUploadStarted = Calendar.getInstance().time + if (contribution.errorInfo == null) { + contribution.chunkInfo = null + contribution.transferred = 0 + } + compositeDisposable.add( + uploadRepository + .checkDuplicateImage(contribution.localUriPath!!.path) + .subscribeOn(ioThreadScheduler) + .subscribe({ imageCheckResult: Int -> + if (imageCheckResult == IMAGE_OK) { + contribution.state = STATE_QUEUED + compositeDisposable.add( + contributionsRepository + .save(contribution) + .subscribeOn(ioThreadScheduler) + .doOnComplete { + restartUploads(contributionList, index + 1, context) + } + .subscribe { + makeOneTimeWorkRequest( + context, ExistingWorkPolicy.KEEP + ) + }) + } else { + Timber.e("Contribution already exists") + compositeDisposable.add( + contributionsRepository + .deleteContributionFromDB(contribution) + .subscribeOn(ioThreadScheduler).doOnComplete { + restartUploads(contributionList, index + 1, context) + } + .subscribe()) + } + }, { throwable: Throwable? -> + Timber.e(throwable) + restartUploads(contributionList, index + 1, context) + }) + ) + } else { + contribution.state = STATE_QUEUED + compositeDisposable.add( + contributionsRepository + .save(contribution) + .subscribeOn(ioThreadScheduler) + .doOnComplete { + restartUploads(contributionList, index + 1, context) + } + .subscribe { + 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. + */ + fun restartUpload(contributionList: List, index: Int, context: Context) { + CommonsApplication.isPaused = false + if (index >= contributionList.size) { + return + } + val contribution = contributionList[index] + if (contribution.state == STATE_FAILED) { + contribution.dateUploadStarted = Calendar.getInstance().time + if (contribution.errorInfo == null) { + contribution.chunkInfo = null + contribution.transferred = 0 + } + compositeDisposable.add( + uploadRepository + .checkDuplicateImage(contribution.localUriPath!!.path) + .subscribeOn(ioThreadScheduler) + .subscribe { imageCheckResult: Int -> + if (imageCheckResult == IMAGE_OK) { + contribution.state = STATE_QUEUED + compositeDisposable.add( + contributionsRepository + .save(contribution) + .subscribeOn(ioThreadScheduler) + .subscribe { + makeOneTimeWorkRequest(context, ExistingWorkPolicy.KEEP) + } + ) + } else { + Timber.e("Contribution already exists") + compositeDisposable.add( + contributionsRepository + .deleteContributionFromDB(contribution) + .subscribeOn(ioThreadScheduler) + .subscribe() + ) + } + }) + } else { + contribution.state = STATE_QUEUED + compositeDisposable.add( + contributionsRepository + .save(contribution) + .subscribeOn(ioThreadScheduler) + .subscribe { + makeOneTimeWorkRequest(context, ExistingWorkPolicy.KEEP) + } + ) + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/ReadFBMD.java b/app/src/main/java/fr/free/nrw/commons/upload/ReadFBMD.java deleted file mode 100644 index 586f9fc24..000000000 --- a/app/src/main/java/fr/free/nrw/commons/upload/ReadFBMD.java +++ /dev/null @@ -1,48 +0,0 @@ -package fr.free.nrw.commons.upload; - -import fr.free.nrw.commons.utils.ImageUtils; -import io.reactivex.Single; -import java.io.FileInputStream; -import java.io.IOException; -import javax.inject.Inject; -import javax.inject.Singleton; - -/** - * We want to discourage users from uploading images to Commons that were taken from Facebook. This - * attempts to detect whether an image was downloaded from Facebook by heuristically searching for - * metadata that is specific to images that come from Facebook. - */ -@Singleton -public class ReadFBMD { - - @Inject - public ReadFBMD() { - } - - public Single processMetadata(String path) { - return Single.fromCallable(() -> { - try { - int psBlockOffset; - int fbmdOffset; - - try (FileInputStream fs = new FileInputStream(path)) { - byte[] bytes = new byte[4096]; - fs.read(bytes); - fs.close(); - String fileStr = new String(bytes); - psBlockOffset = fileStr.indexOf("8BIM"); - fbmdOffset = fileStr.indexOf("FBMD"); - } - - if (psBlockOffset > 0 && fbmdOffset > 0 - && fbmdOffset > psBlockOffset && fbmdOffset - psBlockOffset < 0x80) { - return ImageUtils.FILE_FBMD; - } - } catch (IOException e) { - e.printStackTrace(); - } - return ImageUtils.IMAGE_OK; - }); - } -} - diff --git a/app/src/main/java/fr/free/nrw/commons/upload/ReadFBMD.kt b/app/src/main/java/fr/free/nrw/commons/upload/ReadFBMD.kt new file mode 100644 index 000000000..e4b022826 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/upload/ReadFBMD.kt @@ -0,0 +1,45 @@ +package fr.free.nrw.commons.upload + +import fr.free.nrw.commons.utils.ImageUtils.FILE_FBMD +import fr.free.nrw.commons.utils.ImageUtils.IMAGE_OK +import io.reactivex.Single +import timber.log.Timber +import java.io.FileInputStream +import java.io.IOException +import javax.inject.Inject +import javax.inject.Singleton + +/** + * We want to discourage users from uploading images to Commons that were taken from Facebook. This + * attempts to detect whether an image was downloaded from Facebook by heuristically searching for + * metadata that is specific to images that come from Facebook. + */ +@Singleton +class ReadFBMD @Inject constructor() { + fun processMetadata(path: String?): Single = Single.fromCallable { + var result = IMAGE_OK + try { + var psBlockOffset: Int + var fbmdOffset: Int + + FileInputStream(path).use { fs -> + val bytes = ByteArray(4096) + fs.read(bytes) + with(String(bytes)) { + psBlockOffset = indexOf("8BIM") + fbmdOffset = indexOf("FBMD") + } + } + + result = if (psBlockOffset > 0 && fbmdOffset > 0 && + fbmdOffset > psBlockOffset && + fbmdOffset - psBlockOffset < 0x80 + ) FILE_FBMD else IMAGE_OK + + } catch (e: IOException) { + Timber.e(e) + } + return@fromCallable result + } +} + diff --git a/app/src/main/java/fr/free/nrw/commons/upload/SimilarImageInterface.java b/app/src/main/java/fr/free/nrw/commons/upload/SimilarImageInterface.java deleted file mode 100644 index 50e6e5f27..000000000 --- a/app/src/main/java/fr/free/nrw/commons/upload/SimilarImageInterface.java +++ /dev/null @@ -1,6 +0,0 @@ -package fr.free.nrw.commons.upload; - -public interface SimilarImageInterface { - void showSimilarImageFragment(String originalFilePath, String possibleFilePath, - ImageCoordinates similarImageCoordinates); -} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/SimilarImageInterface.kt b/app/src/main/java/fr/free/nrw/commons/upload/SimilarImageInterface.kt new file mode 100644 index 000000000..ae4805ea2 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/upload/SimilarImageInterface.kt @@ -0,0 +1,9 @@ +package fr.free.nrw.commons.upload + +interface SimilarImageInterface { + fun showSimilarImageFragment( + originalFilePath: String?, + possibleFilePath: String?, + similarImageCoordinates: ImageCoordinates? + ) +} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/ThumbnailClickedListener.java b/app/src/main/java/fr/free/nrw/commons/upload/ThumbnailClickedListener.java deleted file mode 100644 index 371e5ee10..000000000 --- a/app/src/main/java/fr/free/nrw/commons/upload/ThumbnailClickedListener.java +++ /dev/null @@ -1,7 +0,0 @@ -package fr.free.nrw.commons.upload; - -import fr.free.nrw.commons.filepicker.UploadableFile; - -public interface ThumbnailClickedListener { - void thumbnailClicked(UploadableFile content); -} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.java index 659c1e99a..f48773180 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.java @@ -72,7 +72,8 @@ import javax.inject.Inject; import javax.inject.Named; import timber.log.Timber; -public class UploadActivity extends BaseActivity implements UploadContract.View, UploadBaseFragment.Callback, ThumbnailsAdapter.OnThumbnailDeletedListener { +public class UploadActivity extends BaseActivity implements + UploadContract.View, UploadBaseFragment.Callback, ThumbnailsAdapter.OnThumbnailDeletedListener { @Inject ContributionController contributionController; @@ -148,7 +149,7 @@ public class UploadActivity extends BaseActivity implements UploadContract.View, @SuppressLint("CheckResult") @Override - protected void onCreate(Bundle savedInstanceState) { + protected void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); binding = ActivityUploadBinding.inflate(getLayoutInflater()); @@ -160,9 +161,9 @@ public class UploadActivity extends BaseActivity implements UploadContract.View, */ if (savedInstanceState != null) { isFragmentsSaved = true; - List fragmentList = getSupportFragmentManager().getFragments(); + final List fragmentList = getSupportFragmentManager().getFragments(); fragments = new ArrayList<>(); - for (Fragment fragment : fragmentList) { + for (final Fragment fragment : fragmentList) { fragments.add((UploadBaseFragment) fragment); } } @@ -174,8 +175,8 @@ public class UploadActivity extends BaseActivity implements UploadContract.View, nearbyPopupAnswers = new HashMap<>(); //getting the current dpi of the device and if it is less than 320dp i.e. overlapping //threshold, thumbnails automatically minimizes - DisplayMetrics metrics = getResources().getDisplayMetrics(); - float dpi = (metrics.widthPixels)/(metrics.density); + final DisplayMetrics metrics = getResources().getDisplayMetrics(); + final float dpi = (metrics.widthPixels)/(metrics.density); if (dpi<=321) { onRlContainerTitleClicked(); } @@ -217,13 +218,13 @@ public class UploadActivity extends BaseActivity implements UploadContract.View, binding.vpUpload.setAdapter(uploadImagesAdapter); binding.vpUpload.addOnPageChangeListener(new ViewPager.OnPageChangeListener() { @Override - public void onPageScrolled(int position, float positionOffset, - int positionOffsetPixels) { + public void onPageScrolled(final int position, final float positionOffset, + final int positionOffsetPixels) { } @Override - public void onPageSelected(int position) { + public void onPageSelected(final int position) { currentSelectedPosition = position; if (position >= uploadableFiles.size()) { binding.cvContainerTopCard.setVisibility(View.GONE); @@ -235,7 +236,7 @@ public class UploadActivity extends BaseActivity implements UploadContract.View, } @Override - public void onPageScrollStateChanged(int state) { + public void onPageScrollStateChanged(final int state) { } }); @@ -330,7 +331,7 @@ public class UploadActivity extends BaseActivity implements UploadContract.View, * Show/Hide the progress dialog */ @Override - public void showProgress(boolean shouldShow) { + public void showProgress(final boolean shouldShow) { if (shouldShow) { if (!progressDialog.isShowing()) { progressDialog.show(); @@ -343,7 +344,7 @@ public class UploadActivity extends BaseActivity implements UploadContract.View, } @Override - public int getIndexInViewFlipper(UploadBaseFragment fragment) { + public int getIndexInViewFlipper(final UploadBaseFragment fragment) { return fragments.indexOf(fragment); } @@ -358,7 +359,7 @@ public class UploadActivity extends BaseActivity implements UploadContract.View, } @Override - public void showMessage(int messageResourceId) { + public void showMessage(final int messageResourceId) { ViewUtil.showLongToast(this, messageResourceId); } @@ -368,12 +369,12 @@ public class UploadActivity extends BaseActivity implements UploadContract.View, } @Override - public void showHideTopCard(boolean shouldShow) { + public void showHideTopCard(final boolean shouldShow) { binding.llContainerTopCard.setVisibility(shouldShow ? View.VISIBLE : View.GONE); } @Override - public void onUploadMediaDeleted(int index) { + public void onUploadMediaDeleted(final int index) { fragments.remove(index);//Remove the corresponding fragment uploadableFiles.remove(index);//Remove the files from the list thumbnailsAdapter.notifyItemRemoved(index); //Notify the thumbnails adapter @@ -396,7 +397,7 @@ public class UploadActivity extends BaseActivity implements UploadContract.View, public void askUserToLogIn() { Timber.d("current session is null, asking user to login"); ViewUtil.showLongToast(this, getString(R.string.user_not_logged_in)); - Intent loginIntent = new Intent(UploadActivity.this, LoginActivity.class); + final Intent loginIntent = new Intent(UploadActivity.this, LoginActivity.class); startActivity(loginIntent); } @@ -408,10 +409,10 @@ public class UploadActivity extends BaseActivity implements UploadContract.View, if (requestCode == RequestCodes.STORAGE) { if (VERSION.SDK_INT >= VERSION_CODES.M) { for (int i = 0; i < grantResults.length; i++) { - String permission = permissions[i]; + final String permission = permissions[i]; areAllGranted = grantResults[i] == PackageManager.PERMISSION_GRANTED; if (grantResults[i] == PackageManager.PERMISSION_DENIED) { - boolean showRationale = shouldShowRequestPermissionRationale(permission); + final boolean showRationale = shouldShowRequestPermissionRationale(permission); if (!showRationale) { DialogUtil.showAlertDialog(this, getString(R.string.storage_permissions_denied), @@ -442,14 +443,14 @@ public class UploadActivity extends BaseActivity implements UploadContract.View, * * @param uploadOfAPlace a boolean value indicating whether the upload is of place. */ - public static void setUploadIsOfAPlace(boolean uploadOfAPlace) { + public static void setUploadIsOfAPlace(final boolean uploadOfAPlace) { uploadIsOfAPlace = uploadOfAPlace; } private void receiveSharedItems() { - thumbnailsAdapter.context=this; - Intent intent = getIntent(); - String action = intent.getAction(); + ThumbnailsAdapter.context=this; + final Intent intent = getIntent(); + final String action = intent.getAction(); if (Intent.ACTION_SEND.equals(action) || Intent.ACTION_SEND_MULTIPLE.equals(action)) { receiveExternalSharedItems(); } else if (ACTION_INTERNAL_UPLOADS.equals(action)) { @@ -481,8 +482,8 @@ public class UploadActivity extends BaseActivity implements UploadContract.View, } - for (UploadableFile uploadableFile : uploadableFiles) { - UploadMediaDetailFragment uploadMediaDetailFragment = new UploadMediaDetailFragment(); + for (final UploadableFile uploadableFile : uploadableFiles) { + final UploadMediaDetailFragment uploadMediaDetailFragment = new UploadMediaDetailFragment(); if (!uploadIsOfAPlace) { handleLocation(); @@ -492,9 +493,9 @@ public class UploadActivity extends BaseActivity implements UploadContract.View, uploadMediaDetailFragment.setImageToBeUploaded(uploadableFile, place, currLocation); } - UploadMediaDetailFragmentCallback uploadMediaDetailFragmentCallback = new UploadMediaDetailFragmentCallback() { + final UploadMediaDetailFragmentCallback uploadMediaDetailFragmentCallback = new UploadMediaDetailFragmentCallback() { @Override - public void deletePictureAtIndex(int index) { + public void deletePictureAtIndex(final int index) { store.putInt(keyForCurrentUploadImagesSize, (store.getInt(keyForCurrentUploadImagesSize) - 1)); presenter.deletePictureAtIndex(index); @@ -511,29 +512,29 @@ public class UploadActivity extends BaseActivity implements UploadContract.View, * @param filepath The file path of the new thumbnail image. */ @Override - public void changeThumbnail(int index, String filepath) { + public void changeThumbnail(final int index, final String filepath) { uploadableFiles.remove(index); uploadableFiles.add(index, new UploadableFile(new File(filepath))); binding.rvThumbnails.getAdapter().notifyDataSetChanged(); } @Override - public void onNextButtonClicked(int index) { + public void onNextButtonClicked(final int index) { UploadActivity.this.onNextButtonClicked(index); } @Override - public void onPreviousButtonClicked(int index) { + public void onPreviousButtonClicked(final int index) { UploadActivity.this.onPreviousButtonClicked(index); } @Override - public void showProgress(boolean shouldShow) { + public void showProgress(final boolean shouldShow) { UploadActivity.this.showProgress(shouldShow); } @Override - public int getIndexInViewFlipper(UploadBaseFragment fragment) { + public int getIndexInViewFlipper(final UploadBaseFragment fragment) { return fragments.indexOf(fragment); } @@ -549,7 +550,7 @@ public class UploadActivity extends BaseActivity implements UploadContract.View, }; if(isFragmentsSaved){ - UploadMediaDetailFragment fragment = (UploadMediaDetailFragment) fragments.get(0); + final UploadMediaDetailFragment fragment = (UploadMediaDetailFragment) fragments.get(0); fragment.setCallback(uploadMediaDetailFragmentCallback); }else{ uploadMediaDetailFragment.setCallback(uploadMediaDetailFragmentCallback); @@ -562,7 +563,7 @@ public class UploadActivity extends BaseActivity implements UploadContract.View, if(!isFragmentsSaved){ uploadCategoriesFragment = new UploadCategoriesFragment(); if (place != null) { - Bundle categoryBundle = new Bundle(); + final Bundle categoryBundle = new Bundle(); categoryBundle.putString(SELECTED_NEARBY_PLACE_CATEGORY, place.getCategory()); uploadCategoriesFragment.setArguments(categoryBundle); } @@ -570,7 +571,7 @@ public class UploadActivity extends BaseActivity implements UploadContract.View, uploadCategoriesFragment.setCallback(this); depictsFragment = new DepictsFragment(); - Bundle placeBundle = new Bundle(); + final Bundle placeBundle = new Bundle(); placeBundle.putParcelable(SELECTED_NEARBY_PLACE, place); depictsFragment.setArguments(placeBundle); depictsFragment.setCallback(this); @@ -586,7 +587,7 @@ public class UploadActivity extends BaseActivity implements UploadContract.View, for(int i=1;i prefExifTags = defaultKvStore.getStringSet(Prefs.MANAGED_EXIF_TAGS); + final Set prefExifTags = defaultKvStore.getStringSet(Prefs.MANAGED_EXIF_TAGS); if (prefExifTags.contains(getString(R.string.exif_tag_location))) { return false; } @@ -666,7 +666,7 @@ public class UploadActivity extends BaseActivity implements UploadContract.View, * @param maxSize Max size of the {@code uploadableFiles} */ @Override - public void highlightNextImageOnCancelledImage(int index, int maxSize) { + public void highlightNextImageOnCancelledImage(final int index, final int maxSize) { if (binding.vpUpload != null && index < (maxSize)) { binding.vpUpload.setCurrentItem(index + 1, false); binding.vpUpload.setCurrentItem(index, false); @@ -681,8 +681,8 @@ public class UploadActivity extends BaseActivity implements UploadContract.View, * @param isCancelled Is true when user has cancelled upload of any image in current upload */ @Override - public void setImageCancelled(boolean isCancelled) { - BasicKvStore basicKvStore = new BasicKvStore(this,"IsAnyImageCancelled"); + public void setImageCancelled(final boolean isCancelled) { + final BasicKvStore basicKvStore = new BasicKvStore(this,"IsAnyImageCancelled"); basicKvStore.putBoolean("IsAnyImageCancelled", isCancelled); } @@ -690,15 +690,12 @@ public class UploadActivity extends BaseActivity implements UploadContract.View, * Calculate the difference between current location and * location recorded before capturing the image * - * @param currLocation - * @param prevLocation - * @return */ - private float getLocationDifference(LatLng currLocation, LatLng prevLocation) { + private float getLocationDifference(final LatLng currLocation, final LatLng prevLocation) { if (prevLocation == null) { return 0.0f; } - float[] distance = new float[2]; + final float[] distance = new float[2]; Location.distanceBetween( currLocation.getLatitude(), currLocation.getLongitude(), prevLocation.getLatitude(), prevLocation.getLongitude(), distance); @@ -710,7 +707,7 @@ public class UploadActivity extends BaseActivity implements UploadContract.View, } private void receiveInternalSharedItems() { - Intent intent = getIntent(); + final Intent intent = getIntent(); Timber.d("Received intent %s with action %s", intent.toString(), intent.getAction()); @@ -746,7 +743,7 @@ public class UploadActivity extends BaseActivity implements UploadContract.View, @Override - public void showAlertDialog(int messageResourceId, Runnable onPositiveClick) { + public void showAlertDialog(final int messageResourceId, @NonNull final Runnable onPositiveClick) { DialogUtil.showAlertDialog(this, "", getString(messageResourceId), @@ -755,7 +752,7 @@ public class UploadActivity extends BaseActivity implements UploadContract.View, } @Override - public void onNextButtonClicked(int index) { + public void onNextButtonClicked(final int index) { if (index < fragments.size() - 1) { binding.vpUpload.setCurrentItem(index + 1, false); fragments.get(index + 1).onBecameVisible(); @@ -771,7 +768,7 @@ public class UploadActivity extends BaseActivity implements UploadContract.View, } @Override - public void onPreviousButtonClicked(int index) { + public void onPreviousButtonClicked(final int index) { if (index != 0) { binding.vpUpload.setCurrentItem(index - 1, true); fragments.get(index - 1).onBecameVisible(); @@ -786,7 +783,7 @@ public class UploadActivity extends BaseActivity implements UploadContract.View, } @Override - public void onThumbnailDeleted(int position) { + public void onThumbnailDeleted(final int position) { presenter.deletePictureAtIndex(position); } @@ -795,21 +792,22 @@ public class UploadActivity extends BaseActivity implements UploadContract.View, */ - private class UploadImageAdapter extends FragmentStatePagerAdapter { + private static class UploadImageAdapter extends FragmentStatePagerAdapter { List fragments; - public UploadImageAdapter(FragmentManager fragmentManager) { + public UploadImageAdapter(final FragmentManager fragmentManager) { super(fragmentManager); this.fragments = new ArrayList<>(); } - public void setFragments(List fragments) { + public void setFragments(final List fragments) { this.fragments = fragments; notifyDataSetChanged(); } + @NonNull @Override - public Fragment getItem(int position) { + public Fragment getItem(final int position) { return fragments.get(position); } @@ -819,7 +817,7 @@ public class UploadActivity extends BaseActivity implements UploadContract.View, } @Override - public int getItemPosition(Object object) { + public int getItemPosition(@NonNull final Object item) { return PagerAdapter.POSITION_NONE; } } @@ -893,11 +891,11 @@ public class UploadActivity extends BaseActivity implements UploadContract.View, private void showAlertDialogForCategories() { UploadMediaPresenter.isCategoriesDialogShowing = true; // Inflate the custom layout - LayoutInflater inflater = getLayoutInflater(); - View view = inflater.inflate(R.layout.activity_upload_categories_dialog, null); - CheckBox checkBox = view.findViewById(R.id.categories_checkbox); + final LayoutInflater inflater = getLayoutInflater(); + final View view = inflater.inflate(R.layout.activity_upload_categories_dialog, null); + final CheckBox checkBox = view.findViewById(R.id.categories_checkbox); // Create the alert dialog - AlertDialog alertDialog = new AlertDialog.Builder(this) + final AlertDialog alertDialog = new AlertDialog.Builder(this) .setView(view) .setTitle(getString(R.string.multiple_files_depiction_header)) .setMessage(getString(R.string.multiple_files_depiction)) @@ -943,7 +941,7 @@ public class UploadActivity extends BaseActivity implements UploadContract.View, it shows a list of all the apps on the device and allows users to turn battery optimisation off. */ - Intent batteryOptimisationSettingsIntent = new Intent( + final Intent batteryOptimisationSettingsIntent = new Intent( Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS); startActivity(batteryOptimisationSettingsIntent); // calling checkImageQuality after battery dialog is interacted with @@ -964,15 +962,15 @@ public class UploadActivity extends BaseActivity implements UploadContract.View, * conditions are met, returns current location of the user. */ private void handleLocation(){ - LocationPermissionsHelper locationPermissionsHelper = new LocationPermissionsHelper( + final LocationPermissionsHelper locationPermissionsHelper = new LocationPermissionsHelper( this, locationManager, null); if (locationPermissionsHelper.isLocationAccessToAppsTurnedOn()) { currLocation = locationManager.getLastLocation(); } if (currLocation != null) { - float locationDifference = getLocationDifference(currLocation, prevLocation); - boolean isLocationTagUnchecked = isLocationTagUncheckedInTheSettings(); + final float locationDifference = getLocationDifference(currLocation, prevLocation); + final boolean isLocationTagUnchecked = isLocationTagUncheckedInTheSettings(); /* Remove location if the user has unchecked the Location EXIF tag in the Manage EXIF Tags setting or turned "Record location for in-app shots" off. Also, location information is discarded if the difference between diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadClient.kt b/app/src/main/java/fr/free/nrw/commons/upload/UploadClient.kt index 8464c670f..754ae05dd 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadClient.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadClient.kt @@ -7,7 +7,9 @@ import fr.free.nrw.commons.auth.csrf.CsrfTokenClient import fr.free.nrw.commons.contributions.ChunkInfo import fr.free.nrw.commons.contributions.Contribution import fr.free.nrw.commons.contributions.ContributionDao +import fr.free.nrw.commons.di.NetworkingModule import fr.free.nrw.commons.upload.worker.UploadWorker.NotificationUpdateProgressListener +import fr.free.nrw.commons.utils.TimeProvider import fr.free.nrw.commons.wikidata.mwapi.MwException import io.reactivex.Observable import io.reactivex.disposables.CompositeDisposable @@ -26,6 +28,7 @@ import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicReference import javax.inject.Inject +import javax.inject.Named import javax.inject.Singleton @Singleton @@ -33,7 +36,7 @@ class UploadClient @Inject constructor( private val uploadInterface: UploadInterface, - private val csrfTokenClient: CsrfTokenClient, + @Named(NetworkingModule.NAMED_COMMONS_CSRF) private val csrfTokenClient: CsrfTokenClient, private val pageContentsCreator: PageContentsCreator, private val fileUtilsWrapper: FileUtilsWrapper, private val gson: Gson, @@ -66,7 +69,7 @@ class UploadClient val file = contribution.localUriPath val fileChunks = fileUtilsWrapper.getFileChunks(file, chunkSize) - val mediaType = fileUtilsWrapper.getMimeType(file).toMediaTypeOrNull() + val mediaType = fileUtilsWrapper.getMimeType(file)?.toMediaTypeOrNull() val chunkInfo = AtomicReference() if (isStashValid(contribution)) { @@ -278,11 +281,7 @@ class UploadClient Timber.e(throwable, "Exception occurred in uploading file from stash") Observable.error(throwable) } - - fun interface TimeProvider { - fun currentTimeMillis(): Long - } - } +} private fun canProcess( contributionDao: ContributionDao, diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadContract.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadContract.java deleted file mode 100644 index 5f41a17c9..000000000 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadContract.java +++ /dev/null @@ -1,82 +0,0 @@ -package fr.free.nrw.commons.upload; - -import java.util.List; - -import fr.free.nrw.commons.BasePresenter; -import fr.free.nrw.commons.filepicker.UploadableFile; - -/** - * The contract using which the UplaodActivity would communicate with its presenter - */ -public interface UploadContract { - - public interface View { - - boolean isLoggedIn(); - - void finish(); - - void returnToMainActivity(); - - /** - * When submission successful, go to the loadProgressActivity to hint the user this - * submission is valid. And the user will see the upload progress in this activity; - * Fixes: Issue - */ - void goToUploadProgressActivity(); - - void askUserToLogIn(); - - /** - * Changes current image when one image upload is cancelled, to highlight next image in the top thumbnail. - * Fixes: Issue - * - * @param index Index of image to be removed - * @param maxSize Max size of the {@code uploadableFiles} - */ - void highlightNextImageOnCancelledImage(int index, int maxSize); - - /** - * Used to check if user has cancelled upload of any image in current upload - * so that location compare doesn't show up again in same upload. - * Fixes: Issue - * - * @param isCancelled Is true when user has cancelled upload of any image in current upload - */ - void setImageCancelled(boolean isCancelled); - - void showProgress(boolean shouldShow); - - void showMessage(int messageResourceId); - - /** - * Displays an alert with message given by {@code messageResourceId}. - * {@code onPositiveClick} is run after acknowledgement. - */ - void showAlertDialog(int messageResourceId, Runnable onPositiveClick); - - List getUploadableFiles(); - - void showHideTopCard(boolean shouldShow); - - void onUploadMediaDeleted(int index); - - void updateTopCardTitle(); - - void makeUploadRequest(); - } - - public interface UserActionListener extends BasePresenter { - - void handleSubmit(); - - void deletePictureAtIndex(int index); - - /** - * Calls checkImageQuality of UploadMediaPresenter to check image quality of next image - * - * @param uploadItemIndex Index of next image, whose quality is to be checked - */ - void checkImageQuality(int uploadItemIndex); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadContract.kt b/app/src/main/java/fr/free/nrw/commons/upload/UploadContract.kt new file mode 100644 index 000000000..04ab02b8e --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadContract.kt @@ -0,0 +1,77 @@ +package fr.free.nrw.commons.upload + +import fr.free.nrw.commons.BasePresenter +import fr.free.nrw.commons.filepicker.UploadableFile + +/** + * The contract using which the UplaodActivity would communicate with its presenter + */ +interface UploadContract { + interface View { + fun isLoggedIn(): Boolean + + fun finish() + + fun returnToMainActivity() + + /** + * When submission successful, go to the loadProgressActivity to hint the user this + * submission is valid. And the user will see the upload progress in this activity; + * Fixes: [Issue](https://github.com/commons-app/apps-android-commons/issues/5846) + */ + fun goToUploadProgressActivity() + + fun askUserToLogIn() + + /** + * Changes current image when one image upload is cancelled, to highlight next image in the top thumbnail. + * Fixes: [Issue](https://github.com/commons-app/apps-android-commons/issues/5511) + * + * @param index Index of image to be removed + * @param maxSize Max size of the `uploadableFiles` + */ + fun highlightNextImageOnCancelledImage(index: Int, maxSize: Int) + + /** + * Used to check if user has cancelled upload of any image in current upload + * so that location compare doesn't show up again in same upload. + * Fixes: [Issue](https://github.com/commons-app/apps-android-commons/issues/5511) + * + * @param isCancelled Is true when user has cancelled upload of any image in current upload + */ + fun setImageCancelled(isCancelled: Boolean) + + fun showProgress(shouldShow: Boolean) + + fun showMessage(messageResourceId: Int) + + /** + * Displays an alert with message given by `messageResourceId`. + * `onPositiveClick` is run after acknowledgement. + */ + fun showAlertDialog(messageResourceId: Int, onPositiveClick: Runnable) + + fun getUploadableFiles(): List? + + fun showHideTopCard(shouldShow: Boolean) + + fun onUploadMediaDeleted(index: Int) + + fun updateTopCardTitle() + + fun makeUploadRequest() + } + + interface UserActionListener : BasePresenter { + fun handleSubmit() + + fun deletePictureAtIndex(index: Int) + + /** + * Calls checkImageQuality of UploadMediaPresenter to check image quality of next image + * + * @param uploadItemIndex Index of next image, whose quality is to be checked + */ + fun checkImageQuality(uploadItemIndex: Int) + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadModel.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadModel.java index 2611645de..dfb7ae794 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadModel.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadModel.java @@ -187,7 +187,7 @@ public class UploadModel { public Observable buildContributions() { return Observable.fromIterable(items).map(item -> { - String imageSHA1 = FileUtils.getSHA1(context.getContentResolver().openInputStream(item.getContentUri())); + String imageSHA1 = FileUtils.INSTANCE.getSHA1(context.getContentResolver().openInputStream(item.getContentUri())); final Contribution contribution = new Contribution( item, sessionManager, newListOf(selectedDepictions), newListOf(selectedCategories), imageSHA1); diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadModule.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadModule.java deleted file mode 100644 index eccdff333..000000000 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadModule.java +++ /dev/null @@ -1,58 +0,0 @@ -package fr.free.nrw.commons.upload; - -import com.google.gson.Gson; -import dagger.Binds; -import dagger.Module; -import dagger.Provides; -import fr.free.nrw.commons.auth.csrf.CsrfTokenClient; -import fr.free.nrw.commons.contributions.ContributionDao; -import fr.free.nrw.commons.di.NetworkingModule; -import fr.free.nrw.commons.upload.categories.CategoriesContract; -import fr.free.nrw.commons.upload.categories.CategoriesPresenter; -import fr.free.nrw.commons.upload.depicts.DepictsContract; -import fr.free.nrw.commons.upload.depicts.DepictsPresenter; -import fr.free.nrw.commons.upload.license.MediaLicenseContract; -import fr.free.nrw.commons.upload.license.MediaLicensePresenter; -import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailsContract; -import fr.free.nrw.commons.upload.mediaDetails.UploadMediaPresenter; -import javax.inject.Named; - -/** - * The Dagger Module for upload related presenters and (some other objects maybe in future) - */ -@Module -public abstract class UploadModule { - - @Binds - public abstract UploadContract.UserActionListener bindHomePresenter(UploadPresenter - presenter); - - @Binds - public abstract CategoriesContract.UserActionListener bindsCategoriesPresenter( - CategoriesPresenter presenter); - - @Binds - public abstract MediaLicenseContract.UserActionListener bindsMediaLicensePresenter( - MediaLicensePresenter - presenter); - - @Binds - public abstract UploadMediaDetailsContract.UserActionListener bindsUploadMediaPresenter( - UploadMediaPresenter - presenter); - - @Binds - public abstract DepictsContract.UserActionListener bindsDepictsPresenter( - DepictsPresenter - presenter - ); - - @Provides - public static UploadClient provideUploadClient(final UploadInterface uploadInterface, - @Named(NetworkingModule.NAMED_COMMONS_CSRF) final CsrfTokenClient csrfTokenClient, - final PageContentsCreator pageContentsCreator, final FileUtilsWrapper fileUtilsWrapper, - final Gson gson, final ContributionDao contributionDao) { - return new UploadClient(uploadInterface, csrfTokenClient, pageContentsCreator, - fileUtilsWrapper, gson, System::currentTimeMillis, contributionDao); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadModule.kt b/app/src/main/java/fr/free/nrw/commons/upload/UploadModule.kt new file mode 100644 index 000000000..9596391c6 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadModule.kt @@ -0,0 +1,33 @@ +package fr.free.nrw.commons.upload + +import dagger.Binds +import dagger.Module +import fr.free.nrw.commons.upload.categories.CategoriesContract +import fr.free.nrw.commons.upload.categories.CategoriesPresenter +import fr.free.nrw.commons.upload.depicts.DepictsContract +import fr.free.nrw.commons.upload.depicts.DepictsPresenter +import fr.free.nrw.commons.upload.license.MediaLicenseContract +import fr.free.nrw.commons.upload.license.MediaLicensePresenter +import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailsContract +import fr.free.nrw.commons.upload.mediaDetails.UploadMediaPresenter + +/** + * The Dagger Module for upload related presenters and (some other objects maybe in future) + */ +@Module +abstract class UploadModule { + @Binds + abstract fun bindHomePresenter(presenter: UploadPresenter): UploadContract.UserActionListener + + @Binds + abstract fun bindsCategoriesPresenter(presenter: CategoriesPresenter): CategoriesContract.UserActionListener + + @Binds + abstract fun bindsMediaLicensePresenter(presenter: MediaLicensePresenter): MediaLicenseContract.UserActionListener + + @Binds + abstract fun bindsUploadMediaPresenter(presenter: UploadMediaPresenter): UploadMediaDetailsContract.UserActionListener + + @Binds + abstract fun bindsDepictsPresenter(presenter: DepictsPresenter): DepictsContract.UserActionListener +} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadView.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadView.java deleted file mode 100644 index 83b8fdab3..000000000 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadView.java +++ /dev/null @@ -1,88 +0,0 @@ -package fr.free.nrw.commons.upload; - -import android.net.Uri; - -import androidx.annotation.IntDef; - -import java.lang.annotation.Retention; -import java.util.List; - -import fr.free.nrw.commons.location.LatLng; - -import static java.lang.annotation.RetentionPolicy.SOURCE; - -public interface UploadView { - // Dummy implementation of the view interface to allow us to have a 'null object pattern' - // in the presenter and avoid constant NULL checking. -// UploadView DUMMY = (UploadView) Proxy.newProxyInstance(UploadView.class.getClassLoader(), -// new Class[]{UploadView.class}, (proxy, method, methodArgs) -> null); - - - @Retention(SOURCE) - @IntDef({PLEASE_WAIT, TITLE_CARD, CATEGORIES, LICENSE}) - @interface UploadPage {} - - int PLEASE_WAIT = 0; - - int TITLE_CARD = 1; - int CATEGORIES = 2; - int LICENSE = 3; - - boolean checkIfLoggedIn(); - - void updateThumbnails(List uploads); - - void setNextEnabled(boolean available); - - void setSubmitEnabled(boolean available); - - void setPreviousEnabled(boolean available); - - void setTopCardState(boolean state); - - void setRightCardVisibility(boolean visible); - - void setBottomCardState(boolean state); - - void setBackground(Uri mediaUri); - - void setTopCardVisibility(boolean visible); - - void setBottomCardVisibility(boolean visible); - - void setBottomCardVisibility(@UploadPage int page, int uploadCount); - - void updateRightCardContent(boolean gpsPresent); - - void updateBottomCardContent(int currentStep, int stepCount, UploadItem uploadItem, boolean isShowingItem); - - void updateLicenses(List licenses, String selectedLicense); - - void updateLicenseSummary(String selectedLicense, int imageCount); - - void updateTopCardContent(); - - void updateSubtitleVisibility(int imageCount); - - void dismissKeyboard(); - - void showBadPicturePopup(String errorMessage); - - void showDuplicatePicturePopup(); - - void finish(); - - void launchMapActivity(LatLng decCoords); - - void showErrorMessage(int resourceId); - - void initDefaultCategories(); - - void showNoCategorySelectedWarning(); - - void showProgressDialog(); - - void hideProgressDialog(); - - void askUserToLogIn(); -} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/categories/CategoriesContract.java b/app/src/main/java/fr/free/nrw/commons/upload/categories/CategoriesContract.java deleted file mode 100644 index dc53e1a93..000000000 --- a/app/src/main/java/fr/free/nrw/commons/upload/categories/CategoriesContract.java +++ /dev/null @@ -1,97 +0,0 @@ -package fr.free.nrw.commons.upload.categories; - -import android.content.Context; -import androidx.annotation.NonNull; -import androidx.lifecycle.LiveData; -import fr.free.nrw.commons.BasePresenter; -import fr.free.nrw.commons.Media; -import fr.free.nrw.commons.category.CategoryItem; -import java.util.List; - -/** - * The contract with with UploadCategoriesFragment and its presenter would talk to each other - */ -public interface CategoriesContract { - - interface View { - - void showProgress(boolean shouldShow); - - void showError(String error); - - void showError(int stringResourceId); - - void setCategories(List categories); - - void goToNextScreen(); - - void showNoCategorySelected(); - - /** - * Gets existing category names from media - */ - List getExistingCategories(); - - /** - * Returns required context - */ - Context getFragmentContext(); - - /** - * Returns to previous fragment - */ - void goBackToPreviousScreen(); - - /** - * Shows the progress dialog - */ - void showProgressDialog(); - - /** - * Hides the progress dialog - */ - void dismissProgressDialog(); - - /** - * Refreshes the categories - */ - void refreshCategories(); - - - /** - * Navigate the user to Login Activity - */ - void navigateToLoginScreen(); - } - - interface UserActionListener extends BasePresenter { - - void searchForCategories(String query); - - void verifyCategories(); - - void onCategoryItemClicked(CategoryItem categoryItem); - - /** - * Attaches view and media - */ - void onAttachViewWithMedia(@NonNull CategoriesContract.View view, Media media); - - /** - * Clears previous selections - */ - void clearPreviousSelection(); - - /** - * Update the categories - */ - void updateCategories(Media media, String wikiText); - - LiveData> getCategories(); - - void selectCategories(); - - } - - -} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/categories/CategoriesContract.kt b/app/src/main/java/fr/free/nrw/commons/upload/categories/CategoriesContract.kt new file mode 100644 index 000000000..183c7cd93 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/upload/categories/CategoriesContract.kt @@ -0,0 +1,88 @@ +package fr.free.nrw.commons.upload.categories + +import android.content.Context +import androidx.lifecycle.LiveData +import fr.free.nrw.commons.BasePresenter +import fr.free.nrw.commons.Media +import fr.free.nrw.commons.category.CategoryItem + +/** + * The contract with with UploadCategoriesFragment and its presenter would talk to each other + */ +interface CategoriesContract { + interface View { + fun showProgress(shouldShow: Boolean) + + fun showError(error: String?) + + fun showError(stringResourceId: Int) + + fun setCategories(categories: List?) + + fun goToNextScreen() + + fun showNoCategorySelected() + + /** + * Gets existing category names from media + */ + fun getExistingCategories(): List? + + /** + * Returns required context + */ + fun getFragmentContext(): Context + + /** + * Returns to previous fragment + */ + fun goBackToPreviousScreen() + + /** + * Shows the progress dialog + */ + fun showProgressDialog() + + /** + * Hides the progress dialog + */ + fun dismissProgressDialog() + + /** + * Refreshes the categories + */ + fun refreshCategories() + + /** + * Navigate the user to Login Activity + */ + fun navigateToLoginScreen() + } + + interface UserActionListener : BasePresenter { + fun searchForCategories(query: String) + + fun verifyCategories() + + fun onCategoryItemClicked(categoryItem: CategoryItem) + + /** + * Attaches view and media + */ + fun onAttachViewWithMedia(view: View, media: Media) + + /** + * Clears previous selections + */ + fun clearPreviousSelection() + + /** + * Update the categories + */ + fun updateCategories(media: Media, wikiText: String) + + fun getCategories(): LiveData> + + fun selectCategories() + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/categories/CategoriesPresenter.kt b/app/src/main/java/fr/free/nrw/commons/upload/categories/CategoriesPresenter.kt index 210754bf4..dbeeae6ff 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/categories/CategoriesPresenter.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/categories/CategoriesPresenter.kt @@ -8,7 +8,6 @@ import fr.free.nrw.commons.R import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException import fr.free.nrw.commons.category.CategoryEditHelper import fr.free.nrw.commons.category.CategoryItem -import fr.free.nrw.commons.di.CommonsApplicationModule import fr.free.nrw.commons.di.CommonsApplicationModule.Companion.IO_THREAD import fr.free.nrw.commons.di.CommonsApplicationModule.Companion.MAIN_THREAD import fr.free.nrw.commons.repository.UploadRepository @@ -175,7 +174,7 @@ class CategoriesPresenter ) { this.view = view this.media = media - repository.setSelectedExistingCategories(view.existingCategories) + repository.setSelectedExistingCategories(view.getExistingCategories() ?: emptyList()) compositeDisposable.add( searchTerms .observeOn(mainThreadScheduler) @@ -224,11 +223,11 @@ class CategoriesPresenter repository.getSelectedCategories().isNotEmpty() || ( - view.existingCategories != null + view.getExistingCategories() != null && repository.getSelectedExistingCategories().size != - view.existingCategories.size + view.getExistingCategories()?.size ) ) { val selectedCategories: MutableList = @@ -244,7 +243,7 @@ class CategoriesPresenter compositeDisposable.add( categoryEditHelper .makeCategoryEdit( - view.fragmentContext, + view.getFragmentContext(), media, selectedCategories, wikiText, diff --git a/app/src/main/java/fr/free/nrw/commons/upload/categories/UploadCategoriesFragment.java b/app/src/main/java/fr/free/nrw/commons/upload/categories/UploadCategoriesFragment.java index fbc6bc7c5..1436ab714 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/categories/UploadCategoriesFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/categories/UploadCategoriesFragment.java @@ -65,14 +65,14 @@ public class UploadCategoriesFragment extends UploadBaseFragment implements Cate @Nullable @Override - public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, - @Nullable Bundle savedInstanceState) { + public View onCreateView(@NonNull final LayoutInflater inflater, @Nullable final ViewGroup container, + @Nullable final Bundle savedInstanceState) { binding = UploadCategoriesFragmentBinding.inflate(inflater, container, false); return binding.getRoot(); } @Override - public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); final Bundle bundle = getArguments(); if (bundle != null) { @@ -104,8 +104,12 @@ public class UploadCategoriesFragment extends UploadBaseFragment implements Cate setTvSubTitle(); binding.tooltip.setOnClickListener(new OnClickListener() { @Override - public void onClick(View v) { - DialogUtil.showAlertDialog(getActivity(), getString(R.string.categories_activity_title), getString(R.string.categories_tooltip), getString(android.R.string.ok), null); + public void onClick(final View v) { + DialogUtil.showAlertDialog(requireActivity(), + getString(R.string.categories_activity_title), + getString(R.string.categories_tooltip), + getString(android.R.string.ok), + null); } }); if (media == null) { @@ -146,7 +150,7 @@ public class UploadCategoriesFragment extends UploadBaseFragment implements Cate } } - private void searchForCategory(String query) { + private void searchForCategory(final String query) { presenter.searchForCategories(query); } @@ -170,28 +174,28 @@ public class UploadCategoriesFragment extends UploadBaseFragment implements Cate } @Override - public void showProgress(boolean shouldShow) { + public void showProgress(final boolean shouldShow) { if (binding != null) { binding.pbCategories.setVisibility(shouldShow ? View.VISIBLE : View.GONE); } } @Override - public void showError(String error) { + public void showError(final String error) { if (binding != null) { binding.tilContainerSearch.setError(error); } } @Override - public void showError(int stringResourceId) { + public void showError(final int stringResourceId) { if (binding != null) { binding.tilContainerSearch.setError(getString(stringResourceId)); } } @Override - public void setCategories(List categories) { + public void setCategories(final List categories) { if (categories == null) { adapter.clear(); } else { @@ -229,12 +233,12 @@ public class UploadCategoriesFragment extends UploadBaseFragment implements Cate @Override public void showNoCategorySelected() { if (media == null) { - DialogUtil.showAlertDialog(getActivity(), + DialogUtil.showAlertDialog(requireActivity(), getString(R.string.no_categories_selected), getString(R.string.no_categories_selected_warning_desc), getString(R.string.continue_message), getString(R.string.cancel), - () -> goToNextScreen(), + this::goToNextScreen, null); } else { Toast.makeText(requireContext(), getString(R.string.no_categories_selected), @@ -256,6 +260,7 @@ public class UploadCategoriesFragment extends UploadBaseFragment implements Cate /** * Returns required context */ + @NonNull @Override public Context getFragmentContext() { return requireContext(); @@ -306,7 +311,7 @@ public class UploadCategoriesFragment extends UploadBaseFragment implements Cate public void navigateToLoginScreen() { final String username = sessionManager.getUserName(); final CommonsApplication.BaseLogoutListener logoutListener = new CommonsApplication.BaseLogoutListener( - getActivity(), + requireActivity(), requireActivity().getString(R.string.invalid_login_message), username ); diff --git a/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsContract.java b/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsContract.kt similarity index 56% rename from app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsContract.java rename to app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsContract.kt index 1ff79ab27..2279db7a1 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsContract.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsContract.kt @@ -1,129 +1,125 @@ -package fr.free.nrw.commons.upload.depicts; +package fr.free.nrw.commons.upload.depicts -import android.content.Context; -import androidx.annotation.NonNull; -import androidx.lifecycle.LiveData; -import fr.free.nrw.commons.BasePresenter; -import fr.free.nrw.commons.Media; -import fr.free.nrw.commons.upload.structure.depictions.DepictedItem; -import java.util.List; +import android.content.Context +import androidx.lifecycle.LiveData +import fr.free.nrw.commons.BasePresenter +import fr.free.nrw.commons.Media +import fr.free.nrw.commons.upload.structure.depictions.DepictedItem /** * The contract with which DepictsFragment and its presenter would talk to each other */ -public interface DepictsContract { - +interface DepictsContract { interface View { /** * Go to category screen */ - void goToNextScreen(); + fun goToNextScreen() /** * Go to media detail screen */ - void goToPreviousScreen(); + fun goToPreviousScreen() /** * show error in case of no depiction selected */ - void noDepictionSelected(); + fun noDepictionSelected() /** * Show progress/Hide progress depending on the boolean value */ - void showProgress(boolean shouldShow); + fun showProgress(shouldShow: Boolean) /** * decides whether to show error values or not depending on the boolean value */ - void showError(Boolean value); + fun showError(value: Boolean) /** * add depictions to list */ - void setDepictsList(List depictedItemList); + fun setDepictsList(depictedItemList: List) /** * Returns required context */ - Context getFragmentContext(); + fun getFragmentContext(): Context /** * Returns to previous fragment */ - void goBackToPreviousScreen(); + fun goBackToPreviousScreen() /** * Gets existing depictions IDs from media */ - List getExistingDepictions(); + fun getExistingDepictions(): List? /** * Shows the progress dialog */ - void showProgressDialog(); + fun showProgressDialog() /** * Hides the progress dialog */ - void dismissProgressDialog(); + fun dismissProgressDialog() /** * Update the depictions */ - void updateDepicts(); + fun updateDepicts() /** * Navigate the user to Login Activity */ - void navigateToLoginScreen(); + fun navigateToLoginScreen() } - interface UserActionListener extends BasePresenter { - + interface UserActionListener : BasePresenter { /** * Takes to previous screen */ - void onPreviousButtonClicked(); + fun onPreviousButtonClicked() /** * Listener for the depicted items selected from the list */ - void onDepictItemClicked(DepictedItem depictedItem); + fun onDepictItemClicked(depictedItem: DepictedItem) /** * asks the repository to fetch depictions for the query - * @param query + * @param query */ - void searchForDepictions(String query); + fun searchForDepictions(query: String) /** * Selects all associated places (if any) as depictions */ - void selectPlaceDepictions(); + fun selectPlaceDepictions() /** * Check if depictions were selected * from the depiction list */ - void verifyDepictions(); + fun verifyDepictions() /** * Clears previous selections */ - void clearPreviousSelection(); + fun clearPreviousSelection() - LiveData> getDepictedItems(); + fun getDepictedItems(): LiveData> /** * Update the depictions */ - void updateDepictions(Media media); + fun updateDepictions(media: Media) /** * Attaches view and media */ - void onAttachViewWithMedia(@NonNull View view, Media media); + fun onAttachViewWithMedia(view: View, media: Media) } } diff --git a/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsFragment.java b/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsFragment.java index 0bbe4b15a..b5a22a622 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsFragment.java @@ -218,7 +218,7 @@ public class DepictsFragment extends UploadBaseFragment implements DepictsContra } @Override - public void showError(Boolean value) { + public void showError(boolean value) { if (binding == null) { return; } diff --git a/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsPresenter.kt b/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsPresenter.kt index 4502e3434..02890e608 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsPresenter.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsPresenter.kt @@ -6,7 +6,6 @@ import androidx.lifecycle.MutableLiveData import fr.free.nrw.commons.Media import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException import fr.free.nrw.commons.bookmarks.items.BookmarkItemsController -import fr.free.nrw.commons.di.CommonsApplicationModule import fr.free.nrw.commons.di.CommonsApplicationModule.Companion.IO_THREAD import fr.free.nrw.commons.di.CommonsApplicationModule.Companion.MAIN_THREAD import fr.free.nrw.commons.repository.UploadRepository @@ -208,7 +207,7 @@ class DepictsPresenter @SuppressLint("CheckResult") override fun updateDepictions(media: Media) { if (repository.getSelectedDepictions().isNotEmpty() || - repository.getSelectedExistingDepictions().size != view.existingDepictions.size + repository.getSelectedExistingDepictions().size != view.getExistingDepictions()?.size ) { view.showProgressDialog() val selectedDepictions: MutableList = @@ -225,7 +224,7 @@ class DepictsPresenter compositeDisposable.add( depictsHelper - .makeDepictionEdit(view.fragmentContext, media, selectedDepictions) + .makeDepictionEdit(view.getFragmentContext(), media, selectedDepictions) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe({ @@ -256,7 +255,7 @@ class DepictsPresenter ) { this.view = view this.media = media - repository.setSelectedExistingDepictions(view.existingDepictions) + repository.setSelectedExistingDepictions(view.getExistingDepictions() ?: emptyList()) compositeDisposable.add( searchTerm .observeOn(mainThreadScheduler) diff --git a/app/src/main/java/fr/free/nrw/commons/upload/license/MediaLicenseContract.java b/app/src/main/java/fr/free/nrw/commons/upload/license/MediaLicenseContract.java deleted file mode 100644 index bdc09ca13..000000000 --- a/app/src/main/java/fr/free/nrw/commons/upload/license/MediaLicenseContract.java +++ /dev/null @@ -1,28 +0,0 @@ -package fr.free.nrw.commons.upload.license; - -import java.util.List; - -import fr.free.nrw.commons.BasePresenter; - -/** - * The contract with with MediaLicenseFragment and its presenter would talk to each other - */ -public interface MediaLicenseContract { - - interface View { - void setLicenses(List licenses); - - void setSelectedLicense(String license); - - void updateLicenseSummary(String selectedLicense, int numberOfItems); - } - - interface UserActionListener extends BasePresenter { - void getLicenses(); - - void selectLicense(String licenseName); - - boolean isWLMSupportedForThisPlace(); - } - -} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/license/MediaLicenseContract.kt b/app/src/main/java/fr/free/nrw/commons/upload/license/MediaLicenseContract.kt new file mode 100644 index 000000000..27ec1521e --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/upload/license/MediaLicenseContract.kt @@ -0,0 +1,24 @@ +package fr.free.nrw.commons.upload.license + +import fr.free.nrw.commons.BasePresenter + +/** + * The contract with with MediaLicenseFragment and its presenter would talk to each other + */ +interface MediaLicenseContract { + interface View { + fun setLicenses(licenses: List?) + + fun setSelectedLicense(license: String?) + + fun updateLicenseSummary(selectedLicense: String?, numberOfItems: Int?) + } + + interface UserActionListener : BasePresenter { + fun getLicenses() + + fun selectLicense(licenseName: String) + + fun isWLMSupportedForThisPlace(): Boolean + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/license/MediaLicenseFragment.java b/app/src/main/java/fr/free/nrw/commons/upload/license/MediaLicenseFragment.java index 939ad9a95..5fb82f2f6 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/license/MediaLicenseFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/license/MediaLicenseFragment.java @@ -141,7 +141,7 @@ public class MediaLicenseFragment extends UploadBaseFragment implements MediaLic } @Override - public void updateLicenseSummary(String licenseSummary, int numberOfItems) { + public void updateLicenseSummary(String licenseSummary, Integer numberOfItems) { String licenseHyperLink = "" + getString(Utils.licenseNameFor(licenseSummary)) + "
"; diff --git a/app/src/main/java/fr/free/nrw/commons/upload/license/MediaLicensePresenter.java b/app/src/main/java/fr/free/nrw/commons/upload/license/MediaLicensePresenter.java index 79fdf812a..18955636e 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/license/MediaLicensePresenter.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/license/MediaLicensePresenter.java @@ -1,5 +1,6 @@ package fr.free.nrw.commons.upload.license; +import androidx.annotation.NonNull; import fr.free.nrw.commons.Utils; import fr.free.nrw.commons.kvstore.JsonKvStore; import fr.free.nrw.commons.repository.UploadRepository; @@ -27,14 +28,14 @@ public class MediaLicensePresenter implements MediaLicenseContract.UserActionLis private MediaLicenseContract.View view = DUMMY; @Inject - public MediaLicensePresenter(UploadRepository uploadRepository, - @Named("default_preferences") JsonKvStore defaultKVStore) { + public MediaLicensePresenter(final UploadRepository uploadRepository, + @Named("default_preferences") final JsonKvStore defaultKVStore) { this.repository = uploadRepository; this.defaultKVStore = defaultKVStore; } @Override - public void onAttachView(View view) { + public void onAttachView(@NonNull final View view) { this.view = view; } @@ -48,15 +49,15 @@ public class MediaLicensePresenter implements MediaLicenseContract.UserActionLis */ @Override public void getLicenses() { - List licenses = repository.getLicenses(); + final List licenses = repository.getLicenses(); view.setLicenses(licenses); String selectedLicense = defaultKVStore.getString(Prefs.DEFAULT_LICENSE, Prefs.Licenses.CC_BY_SA_4);//CC_BY_SA_4 is the default one used by the commons web app try {//I have to make sure that the stored default license was not one of the deprecated one's Utils.licenseNameFor(selectedLicense); - } catch (IllegalStateException exception) { - Timber.e(exception.getMessage()); + } catch (final IllegalStateException exception) { + Timber.e(exception); selectedLicense = Prefs.Licenses.CC_BY_SA_4; defaultKVStore.putString(Prefs.DEFAULT_LICENSE, Prefs.Licenses.CC_BY_SA_4); } @@ -70,7 +71,7 @@ public class MediaLicensePresenter implements MediaLicenseContract.UserActionLis * @param licenseName */ @Override - public void selectLicense(String licenseName) { + public void selectLicense(final String licenseName) { repository.setSelectedLicense(licenseName); view.updateLicenseSummary(repository.getSelectedLicense(), repository.getCount()); } diff --git a/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailsContract.java b/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailsContract.java deleted file mode 100644 index 9b789e046..000000000 --- a/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailsContract.java +++ /dev/null @@ -1,115 +0,0 @@ -package fr.free.nrw.commons.upload.mediaDetails; - -import android.app.Activity; -import fr.free.nrw.commons.BasePresenter; -import fr.free.nrw.commons.filepicker.UploadableFile; -import fr.free.nrw.commons.location.LatLng; -import fr.free.nrw.commons.nearby.Place; -import fr.free.nrw.commons.upload.ImageCoordinates; -import fr.free.nrw.commons.upload.SimilarImageInterface; -import fr.free.nrw.commons.upload.UploadMediaDetail; -import fr.free.nrw.commons.upload.UploadItem; -import java.util.List; - -/** - * The contract with with UploadMediaDetails and its presenter would talk to each other - */ -public interface UploadMediaDetailsContract { - - interface View extends SimilarImageInterface { - - void onImageProcessed(UploadItem uploadItem, Place place); - - void onNearbyPlaceFound(UploadItem uploadItem, Place place); - - void showProgress(boolean shouldShow); - - void onImageValidationSuccess(); - - void showMessage(int stringResourceId, int colorResourceId); - - void showMessage(String message, int colorResourceId); - - void showDuplicatePicturePopup(UploadItem uploadItem); - - /** - * Shows a dialog alerting the user that internet connection is required for upload process - * Recalls UploadMediaPresenter.getImageQuality for all the next upload items, - * if there is network connectivity and then the user presses okay - */ - void showConnectionErrorPopup(); - - /** - * Shows a dialog alerting the user that internet connection is required for upload process - * Does nothing if there is network connectivity and then the user presses okay - */ - void showConnectionErrorPopupForCaptionCheck(); - - void showExternalMap(UploadItem uploadItem); - - void showEditActivity(UploadItem uploadItem); - - void updateMediaDetails(List uploadMediaDetails); - - void displayAddLocationDialog(Runnable runnable); - } - - interface UserActionListener extends BasePresenter { - - void receiveImage(UploadableFile uploadableFile, Place place, LatLng inAppPictureLocation); - - void setUploadMediaDetails(List uploadMediaDetails, int uploadItemIndex); - - /** - * Calculates the image quality - * - * @param uploadItemIndex Index of the UploadItem whose quality is to be checked - * @param inAppPictureLocation In app picture location (if any) - * @param activity Context reference - * @return true if no internal error occurs, else returns false - */ - boolean getImageQuality(int uploadItemIndex, LatLng inAppPictureLocation, Activity activity); - - /** - * Checks if the image has a location. Displays a dialog alerting user that no location has - * been to added to the image and asking them to add one, if location was not removed by the - * user - * - * @param uploadItemIndex Index of the uploadItem which has no location - * @param inAppPictureLocation In app picture location (if any) - * @param hasUserRemovedLocation True if user has removed location from the image - */ - void displayLocDialog(int uploadItemIndex, LatLng inAppPictureLocation, - boolean hasUserRemovedLocation); - - /** - * Used to check image quality from stored qualities and display dialogs - * - * @param uploadItem UploadItem whose quality is to be checked - * @param index Index of the UploadItem whose quality is to be checked - */ - void checkImageQuality(UploadItem uploadItem, int index); - - /** - * Updates the image qualities stored in JSON, whenever an image is deleted - * - * @param size Size of uploadableFiles - * @param index Index of the UploadItem which was deleted - */ - void updateImageQualitiesJSON(int size, int index); - - - void copyTitleAndDescriptionToSubsequentMedia(int indexInViewFlipper); - - void fetchTitleAndDescription(int indexInViewFlipper); - - void useSimilarPictureCoordinates(ImageCoordinates imageCoordinates, int uploadItemIndex); - - void onMapIconClicked(int indexInViewFlipper); - - void onEditButtonClicked(int indexInViewFlipper); - - void onUserConfirmedUploadIsOfPlace(Place place); - } - -} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailsContract.kt b/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailsContract.kt new file mode 100644 index 000000000..ca4d8e67b --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailsContract.kt @@ -0,0 +1,122 @@ +package fr.free.nrw.commons.upload.mediaDetails + +import android.app.Activity +import fr.free.nrw.commons.BasePresenter +import fr.free.nrw.commons.filepicker.UploadableFile +import fr.free.nrw.commons.location.LatLng +import fr.free.nrw.commons.nearby.Place +import fr.free.nrw.commons.upload.ImageCoordinates +import fr.free.nrw.commons.upload.SimilarImageInterface +import fr.free.nrw.commons.upload.UploadItem +import fr.free.nrw.commons.upload.UploadMediaDetail + +/** + * The contract with with UploadMediaDetails and its presenter would talk to each other + */ +interface UploadMediaDetailsContract { + interface View : SimilarImageInterface { + fun onImageProcessed(uploadItem: UploadItem?, place: Place?) + + fun onNearbyPlaceFound(uploadItem: UploadItem?, place: Place?) + + fun showProgress(shouldShow: Boolean) + + fun onImageValidationSuccess() + + fun showMessage(stringResourceId: Int, colorResourceId: Int) + + fun showMessage(message: String?, colorResourceId: Int) + + fun showDuplicatePicturePopup(uploadItem: UploadItem?) + + /** + * Shows a dialog alerting the user that internet connection is required for upload process + * Recalls UploadMediaPresenter.getImageQuality for all the next upload items, + * if there is network connectivity and then the user presses okay + */ + fun showConnectionErrorPopup() + + /** + * Shows a dialog alerting the user that internet connection is required for upload process + * Does nothing if there is network connectivity and then the user presses okay + */ + fun showConnectionErrorPopupForCaptionCheck() + + fun showExternalMap(uploadItem: UploadItem?) + + fun showEditActivity(uploadItem: UploadItem?) + + fun updateMediaDetails(uploadMediaDetails: List?) + + fun displayAddLocationDialog(runnable: Runnable?) + } + + interface UserActionListener : BasePresenter { + fun receiveImage( + uploadableFile: UploadableFile?, + place: Place?, + inAppPictureLocation: LatLng? + ) + + fun setUploadMediaDetails( + uploadMediaDetails: List?, + uploadItemIndex: Int + ) + + /** + * Calculates the image quality + * + * @param uploadItemIndex Index of the UploadItem whose quality is to be checked + * @param inAppPictureLocation In app picture location (if any) + * @param activity Context reference + * @return true if no internal error occurs, else returns false + */ + fun getImageQuality( + uploadItemIndex: Int, + inAppPictureLocation: LatLng?, + activity: Activity? + ): Boolean + + /** + * Checks if the image has a location. Displays a dialog alerting user that no location has + * been to added to the image and asking them to add one, if location was not removed by the + * user + * + * @param uploadItemIndex Index of the uploadItem which has no location + * @param inAppPictureLocation In app picture location (if any) + * @param hasUserRemovedLocation True if user has removed location from the image + */ + fun displayLocDialog( + uploadItemIndex: Int, inAppPictureLocation: LatLng?, + hasUserRemovedLocation: Boolean + ) + + /** + * Used to check image quality from stored qualities and display dialogs + * + * @param uploadItem UploadItem whose quality is to be checked + * @param index Index of the UploadItem whose quality is to be checked + */ + fun checkImageQuality(uploadItem: UploadItem?, index: Int) + + /** + * Updates the image qualities stored in JSON, whenever an image is deleted + * + * @param size Size of uploadableFiles + * @param index Index of the UploadItem which was deleted + */ + fun updateImageQualitiesJSON(size: Int, index: Int) + + fun copyTitleAndDescriptionToSubsequentMedia(indexInViewFlipper: Int) + + fun fetchTitleAndDescription(indexInViewFlipper: Int) + + fun useSimilarPictureCoordinates(imageCoordinates: ImageCoordinates?, uploadItemIndex: Int) + + fun onMapIconClicked(indexInViewFlipper: Int) + + fun onEditButtonClicked(indexInViewFlipper: Int) + + fun onUserConfirmedUploadIsOfPlace(place: Place?) + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaPresenter.java b/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaPresenter.java index cd533401b..6c234cdaf 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaPresenter.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaPresenter.java @@ -79,10 +79,10 @@ public class UploadMediaPresenter implements UserActionListener, SimilarImageInt public static boolean isCategoriesDialogShowing; @Inject - public UploadMediaPresenter(UploadRepository uploadRepository, - @Named("default_preferences") JsonKvStore defaultKVStore, - @Named(IO_THREAD) Scheduler ioScheduler, - @Named(MAIN_THREAD) Scheduler mainThreadScheduler) { + public UploadMediaPresenter(final UploadRepository uploadRepository, + @Named("default_preferences") final JsonKvStore defaultKVStore, + @Named(IO_THREAD) final Scheduler ioScheduler, + @Named(MAIN_THREAD) final Scheduler mainThreadScheduler) { this.repository = uploadRepository; this.defaultKVStore = defaultKVStore; this.ioScheduler = ioScheduler; @@ -91,7 +91,7 @@ public class UploadMediaPresenter implements UserActionListener, SimilarImageInt } @Override - public void onAttachView(View view) { + public void onAttachView(final View view) { this.view = view; } @@ -103,23 +103,18 @@ public class UploadMediaPresenter implements UserActionListener, SimilarImageInt /** * Sets the Upload Media Details for the corresponding upload item - * - * @param uploadMediaDetails - * @param uploadItemIndex */ @Override - public void setUploadMediaDetails(List uploadMediaDetails, int uploadItemIndex) { + public void setUploadMediaDetails(final List uploadMediaDetails, final int uploadItemIndex) { repository.getUploads().get(uploadItemIndex).setMediaDetails(uploadMediaDetails); } /** * Receives the corresponding uploadable file, processes it and return the view with and uplaod item - * @param uploadableFile - * @param place */ @Override public void receiveImage(final UploadableFile uploadableFile, final Place place, - LatLng inAppPictureLocation) { + final LatLng inAppPictureLocation) { view.showProgress(true); compositeDisposable.add( repository @@ -186,7 +181,6 @@ public class UploadMediaPresenter implements UserActionListener, SimilarImageInt /** * This method checks for the nearest location that needs images and suggests it to the user. - * @param uploadItem */ private void checkNearbyPlaces(final UploadItem uploadItem) { final Disposable checkNearbyPlaces = Maybe.fromCallable(() -> repository @@ -213,10 +207,10 @@ public class UploadMediaPresenter implements UserActionListener, SimilarImageInt * @param hasUserRemovedLocation True if user has removed location from the image */ @Override - public void displayLocDialog(int uploadItemIndex, LatLng inAppPictureLocation, - boolean hasUserRemovedLocation) { + public void displayLocDialog(final int uploadItemIndex, final LatLng inAppPictureLocation, + final boolean hasUserRemovedLocation) { final List uploadItems = repository.getUploads(); - UploadItem uploadItem = uploadItems.get(uploadItemIndex); + final UploadItem uploadItem = uploadItems.get(uploadItemIndex); if (uploadItem.getGpsCoords().getDecimalCoords() == null && inAppPictureLocation == null && !hasUserRemovedLocation) { final Runnable onSkipClicked = () -> { @@ -233,7 +227,7 @@ public class UploadMediaPresenter implements UserActionListener, SimilarImageInt * * @param uploadItem UploadItem whose caption is checked */ - private void verifyCaptionQuality(UploadItem uploadItem) { + private void verifyCaptionQuality(final UploadItem uploadItem) { view.showProgress(true); compositeDisposable.add( repository @@ -262,7 +256,7 @@ public class UploadMediaPresenter implements UserActionListener, SimilarImageInt * @param errorCode Error code of the UploadItem * @param uploadItem UploadItem whose caption is checked */ - public void handleCaptionResult(Integer errorCode, UploadItem uploadItem) { + public void handleCaptionResult(final Integer errorCode, final UploadItem uploadItem) { // If errorCode is empty caption show message if (errorCode == EMPTY_CAPTION) { Timber.d("Captions are empty. Showing toast"); @@ -285,11 +279,9 @@ public class UploadMediaPresenter implements UserActionListener, SimilarImageInt /** * Copies the caption and description of the current item to the subsequent media - * - * @param indexInViewFlipper */ @Override - public void copyTitleAndDescriptionToSubsequentMedia(int indexInViewFlipper) { + public void copyTitleAndDescriptionToSubsequentMedia(final int indexInViewFlipper) { for(int i = indexInViewFlipper+1; i < repository.getCount(); i++){ final UploadItem subsequentUploadItem = repository.getUploads().get(i); subsequentUploadItem.setMediaDetails(deepCopy(repository.getUploads().get(indexInViewFlipper).getUploadMediaDetails())); @@ -298,36 +290,34 @@ public class UploadMediaPresenter implements UserActionListener, SimilarImageInt /** * Fetches and set the caption and description of the item - * - * @param indexInViewFlipper */ @Override - public void fetchTitleAndDescription(int indexInViewFlipper) { + public void fetchTitleAndDescription(final int indexInViewFlipper) { final UploadItem currentUploadItem = repository.getUploads().get(indexInViewFlipper); view.updateMediaDetails(currentUploadItem.getUploadMediaDetails()); } @NotNull - private List deepCopy(List uploadMediaDetails) { + private List deepCopy(final List uploadMediaDetails) { final ArrayList newList = new ArrayList<>(); - for (UploadMediaDetail uploadMediaDetail : uploadMediaDetails) { + for (final UploadMediaDetail uploadMediaDetail : uploadMediaDetails) { newList.add(uploadMediaDetail.javaCopy()); } return newList; } @Override - public void useSimilarPictureCoordinates(ImageCoordinates imageCoordinates, int uploadItemIndex) { + public void useSimilarPictureCoordinates(final ImageCoordinates imageCoordinates, final int uploadItemIndex) { repository.useSimilarPictureCoordinates(imageCoordinates, uploadItemIndex); } @Override - public void onMapIconClicked(int indexInViewFlipper) { + public void onMapIconClicked(final int indexInViewFlipper) { view.showExternalMap(repository.getUploads().get(indexInViewFlipper)); } @Override - public void onEditButtonClicked(int indexInViewFlipper){ + public void onEditButtonClicked(final int indexInViewFlipper){ view.showEditActivity(repository.getUploads().get(indexInViewFlipper)); } @@ -338,9 +328,9 @@ public class UploadMediaPresenter implements UserActionListener, SimilarImageInt * @param place The place to be associated with the uploads. */ @Override - public void onUserConfirmedUploadIsOfPlace(Place place) { + public void onUserConfirmedUploadIsOfPlace(final Place place) { final List uploads = repository.getUploads(); - for (UploadItem uploadItem : uploads) { + for (final UploadItem uploadItem : uploads) { uploadItem.setPlace(place); final List uploadMediaDetails = uploadItem.getUploadMediaDetails(); // Update UploadMediaDetail object for this UploadItem @@ -362,11 +352,11 @@ public class UploadMediaPresenter implements UserActionListener, SimilarImageInt * @return true if no internal error occurs, else returns false */ @Override - public boolean getImageQuality(int uploadItemIndex, LatLng inAppPictureLocation, - Activity activity) { + public boolean getImageQuality(final int uploadItemIndex, final LatLng inAppPictureLocation, + final Activity activity) { final List uploadItems = repository.getUploads(); view.showProgress(true); - if (uploadItems.size() == 0) { + if (uploadItems.isEmpty()) { view.showProgress(false); // No internationalization required for this error message because it's an internal error. view.showMessage( @@ -374,7 +364,7 @@ public class UploadMediaPresenter implements UserActionListener, SimilarImageInt R.color.color_error); return false; } - UploadItem uploadItem = uploadItems.get(uploadItemIndex); + final UploadItem uploadItem = uploadItems.get(uploadItemIndex); compositeDisposable.add( repository .getImageQuality(uploadItem, inAppPictureLocation) @@ -404,12 +394,12 @@ public class UploadMediaPresenter implements UserActionListener, SimilarImageInt * @param activity Context reference * @param uploadItem UploadItem whose quality is to be checked */ - private void storeImageQuality(Integer imageResult, int uploadItemIndex, Activity activity, - UploadItem uploadItem) { - BasicKvStore store = new BasicKvStore(activity, + private void storeImageQuality(final Integer imageResult, final int uploadItemIndex, final Activity activity, + final UploadItem uploadItem) { + final BasicKvStore store = new BasicKvStore(activity, UploadActivity.storeNameForCurrentUploadImagesSize); - String value = store.getString(keyForCurrentUploadImageQualities, null); - JSONObject jsonObject; + final String value = store.getString(keyForCurrentUploadImageQualities, null); + final JSONObject jsonObject; try { if (value != null) { jsonObject = new JSONObject(value); @@ -418,7 +408,8 @@ public class UploadMediaPresenter implements UserActionListener, SimilarImageInt } jsonObject.put("UploadItem" + uploadItemIndex, imageResult); store.putString(keyForCurrentUploadImageQualities, jsonObject.toString()); - } catch (Exception e) { + } catch (final Exception e) { + Timber.e(e); } if (uploadItemIndex == 0) { @@ -438,20 +429,20 @@ public class UploadMediaPresenter implements UserActionListener, SimilarImageInt * @param index Index of the UploadItem whose quality is to be checked */ @Override - public void checkImageQuality(UploadItem uploadItem, int index) { + public void checkImageQuality(final UploadItem uploadItem, final int index) { if ((uploadItem.getImageQuality() != IMAGE_OK) && (uploadItem.getImageQuality() != IMAGE_KEEP)) { - BasicKvStore store = new BasicKvStore(activity, + final BasicKvStore store = new BasicKvStore(activity, UploadActivity.storeNameForCurrentUploadImagesSize); - String value = store.getString(keyForCurrentUploadImageQualities, null); - JSONObject jsonObject; + final String value = store.getString(keyForCurrentUploadImageQualities, null); + final JSONObject jsonObject; try { if (value != null) { jsonObject = new JSONObject(value); } else { jsonObject = new JSONObject(); } - Integer imageQuality = (int) jsonObject.get("UploadItem" + index); + final Integer imageQuality = (int) jsonObject.get("UploadItem" + index); view.showProgress(false); if (imageQuality == IMAGE_OK) { uploadItem.setHasInvalidLocation(false); @@ -459,7 +450,7 @@ public class UploadMediaPresenter implements UserActionListener, SimilarImageInt } else { handleBadImage(imageQuality, uploadItem, index); } - } catch (Exception e) { + } catch (final Exception e) { } } } @@ -471,11 +462,11 @@ public class UploadMediaPresenter implements UserActionListener, SimilarImageInt * @param index Index of the UploadItem which was deleted */ @Override - public void updateImageQualitiesJSON(int size, int index) { - BasicKvStore store = new BasicKvStore(activity, + public void updateImageQualitiesJSON(final int size, final int index) { + final BasicKvStore store = new BasicKvStore(activity, UploadActivity.storeNameForCurrentUploadImagesSize); - String value = store.getString(keyForCurrentUploadImageQualities, null); - JSONObject jsonObject; + final String value = store.getString(keyForCurrentUploadImageQualities, null); + final JSONObject jsonObject; try { if (value != null) { jsonObject = new JSONObject(value); @@ -487,7 +478,8 @@ public class UploadMediaPresenter implements UserActionListener, SimilarImageInt } jsonObject.remove("UploadItem" + (size - 1)); store.putString(keyForCurrentUploadImageQualities, jsonObject.toString()); - } catch (Exception e) { + } catch (final Exception e) { + Timber.e(e); } } @@ -498,8 +490,8 @@ public class UploadMediaPresenter implements UserActionListener, SimilarImageInt * @param uploadItem UploadItem whose quality is bad * @param index Index of item whose quality is bad */ - public void handleBadImage(Integer errorCode, - UploadItem uploadItem, int index) { + public void handleBadImage(final Integer errorCode, + final UploadItem uploadItem, final int index) { Timber.d("Handle bad picture with error code %d", errorCode); if (errorCode >= 8) { // If location of image and nearby does not match uploadItem.setHasInvalidLocation(true); @@ -520,9 +512,9 @@ public class UploadMediaPresenter implements UserActionListener, SimilarImageInt * @param activity Context reference * @param uploadItem UploadItem which has problems */ - public void showBadImagePopup(Integer errorCode, - int index, Activity activity, UploadItem uploadItem) { - String errorMessageForResult = getErrorMessageForResult(activity, errorCode); + public void showBadImagePopup(final Integer errorCode, + final int index, final Activity activity, final UploadItem uploadItem) { + final String errorMessageForResult = getErrorMessageForResult(activity, errorCode); if (!StringUtils.isBlank(errorMessageForResult)) { DialogUtil.showAlertDialog(activity, activity.getString(R.string.upload_problem_image), @@ -537,20 +529,16 @@ public class UploadMediaPresenter implements UserActionListener, SimilarImageInt presenterCallback.deletePictureAtIndex(index); } ).setCancelable(false); - } else { } //If the error message is null, we will probably not show anything } /** * notifies the user that a similar image exists - * @param originalFilePath - * @param possibleFilePath - * @param similarImageCoordinates */ @Override - public void showSimilarImageFragment(String originalFilePath, String possibleFilePath, - ImageCoordinates similarImageCoordinates) { + public void showSimilarImageFragment(final String originalFilePath, final String possibleFilePath, + final ImageCoordinates similarImageCoordinates) { view.showSimilarImageFragment(originalFilePath, possibleFilePath, similarImageCoordinates ); diff --git a/app/src/main/java/fr/free/nrw/commons/upload/structure/depictions/UploadDepictsCallback.java b/app/src/main/java/fr/free/nrw/commons/upload/structure/depictions/UploadDepictsCallback.java deleted file mode 100644 index d176f9c18..000000000 --- a/app/src/main/java/fr/free/nrw/commons/upload/structure/depictions/UploadDepictsCallback.java +++ /dev/null @@ -1,8 +0,0 @@ -package fr.free.nrw.commons.upload.structure.depictions; - -/** - * Listener to trigger callback whenever a depicts item is clicked - */ -public interface UploadDepictsCallback { - void depictsClicked(DepictedItem item); -} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/ConfigUtils.kt b/app/src/main/java/fr/free/nrw/commons/utils/ConfigUtils.kt index bfd0bbb6b..332c8d023 100644 --- a/app/src/main/java/fr/free/nrw/commons/utils/ConfigUtils.kt +++ b/app/src/main/java/fr/free/nrw/commons/utils/ConfigUtils.kt @@ -4,6 +4,9 @@ import android.content.Context import android.content.pm.PackageManager import fr.free.nrw.commons.BuildConfig +// TODO - this can be constructed in a Dagger provider method, in a module and injected. No need +// to compute these values every time, and it means we can avoid having a Context in various +// other places in the app. object ConfigUtils { @JvmStatic val isBetaFlavour: Boolean = BuildConfig.FLAVOR == "beta" diff --git a/app/src/main/java/fr/free/nrw/commons/utils/TimeProvider.kt b/app/src/main/java/fr/free/nrw/commons/utils/TimeProvider.kt new file mode 100644 index 000000000..e09b8fb45 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/TimeProvider.kt @@ -0,0 +1,5 @@ +package fr.free.nrw.commons.utils + +fun interface TimeProvider { + fun currentTimeMillis(): Long +} \ No newline at end of file diff --git a/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadActivityUnitTests.kt b/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadActivityUnitTests.kt index 938e595af..1173d09b0 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadActivityUnitTests.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadActivityUnitTests.kt @@ -5,7 +5,6 @@ import android.content.Intent import androidx.test.core.app.ApplicationProvider import androidx.work.Configuration import androidx.work.testing.WorkManagerTestInitHelper -import fr.free.nrw.commons.CommonsApplication import fr.free.nrw.commons.OkHttpConnectionFactory import fr.free.nrw.commons.R import fr.free.nrw.commons.TestCommonsApplication @@ -75,7 +74,7 @@ class UploadActivityUnitTests { @Test @Throws(Exception::class) fun testIsLoggedIn() { - activity.isLoggedIn + activity.isLoggedIn() } @Test @@ -139,7 +138,7 @@ class UploadActivityUnitTests { @Test @Throws(Exception::class) fun testGetUploadableFiles() { - activity.uploadableFiles + activity.getUploadableFiles() } @Test diff --git a/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadClientTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadClientTest.kt index 50130106a..285c69233 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadClientTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadClientTest.kt @@ -8,14 +8,13 @@ import com.nhaarman.mockitokotlin2.anyOrNull import com.nhaarman.mockitokotlin2.argumentCaptor import com.nhaarman.mockitokotlin2.eq import com.nhaarman.mockitokotlin2.mock -import com.nhaarman.mockitokotlin2.times import com.nhaarman.mockitokotlin2.whenever import fr.free.nrw.commons.CommonsApplication.Companion.DEFAULT_EDIT_SUMMARY import fr.free.nrw.commons.auth.csrf.CsrfTokenClient import fr.free.nrw.commons.contributions.ChunkInfo import fr.free.nrw.commons.contributions.Contribution import fr.free.nrw.commons.contributions.ContributionDao -import fr.free.nrw.commons.upload.UploadClient.TimeProvider +import fr.free.nrw.commons.utils.TimeProvider import fr.free.nrw.commons.wikidata.mwapi.MwException import fr.free.nrw.commons.wikidata.mwapi.MwServiceError import io.reactivex.Observable diff --git a/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadPresenterTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadPresenterTest.kt index 861d1a6a4..8741c2da8 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadPresenterTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadPresenterTest.kt @@ -61,7 +61,7 @@ class UploadPresenterTest { uploadPresenter.onAttachView(view) `when`(repository.buildContributions()).thenReturn(Observable.just(contribution)) uploadableFiles.add(uploadableFile) - `when`(view.uploadableFiles).thenReturn(uploadableFiles) + `when`(view.getUploadableFiles()).thenReturn(uploadableFiles) `when`(uploadableFile.getFilePath()).thenReturn("data://test") } @@ -71,9 +71,9 @@ class UploadPresenterTest { @Ignore @Test fun handleSubmitTestUserLoggedIn() { - `when`(view.isLoggedIn).thenReturn(true) + `when`(view.isLoggedIn()).thenReturn(true) uploadPresenter.handleSubmit() - verify(view).isLoggedIn + verify(view).isLoggedIn() verify(view).showProgress(true) verify(repository).buildContributions() verify(repository).buildContributions() @@ -130,9 +130,9 @@ class UploadPresenterTest { false, ), ).thenReturn(true) - `when`(view.isLoggedIn).thenReturn(true) + `when`(view.isLoggedIn()).thenReturn(true) uploadPresenter.handleSubmit() - verify(view).isLoggedIn + verify(view).isLoggedIn() verify(view).showProgress(true) verify(repository).buildContributions() verify(repository).buildContributions() @@ -144,9 +144,9 @@ class UploadPresenterTest { @Ignore @Test fun handleSubmitTestUserNotLoggedIn() { - `when`(view.isLoggedIn).thenReturn(false) + `when`(view.isLoggedIn()).thenReturn(false) uploadPresenter.handleSubmit() - verify(view).isLoggedIn + verify(view).isLoggedIn() verify(view).askUserToLogIn() } diff --git a/app/src/test/kotlin/fr/free/nrw/commons/upload/categories/UploadCategoriesFragmentUnitTests.kt b/app/src/test/kotlin/fr/free/nrw/commons/upload/categories/UploadCategoriesFragmentUnitTests.kt index ee2d54ee7..f2d54132c 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/upload/categories/UploadCategoriesFragmentUnitTests.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/upload/categories/UploadCategoriesFragmentUnitTests.kt @@ -6,7 +6,6 @@ import android.view.LayoutInflater import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentTransaction import androidx.test.core.app.ApplicationProvider -import com.nhaarman.mockitokotlin2.times import fr.free.nrw.commons.Media import fr.free.nrw.commons.OkHttpConnectionFactory import fr.free.nrw.commons.R @@ -184,14 +183,14 @@ class UploadCategoriesFragmentUnitTests { @Throws(Exception::class) fun testGetExistingCategories() { Shadows.shadowOf(Looper.getMainLooper()).idle() - fragment.existingCategories + fragment.getExistingCategories() } @Test @Throws(Exception::class) fun testGetFragmentContext() { Shadows.shadowOf(Looper.getMainLooper()).idle() - fragment.fragmentContext + fragment.getFragmentContext() } @Test diff --git a/app/src/test/kotlin/fr/free/nrw/commons/upload/depicts/DepictsFragmentUnitTests.kt b/app/src/test/kotlin/fr/free/nrw/commons/upload/depicts/DepictsFragmentUnitTests.kt index 84b878aee..e10ad2ba9 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/upload/depicts/DepictsFragmentUnitTests.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/upload/depicts/DepictsFragmentUnitTests.kt @@ -258,7 +258,7 @@ class DepictsFragmentUnitTests { @Test @Throws(Exception::class) fun testGetFragmentContext() { - fragment.fragmentContext + fragment.getFragmentContext() } @Test