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 <nicolas.raoul@gmail.com>
This commit is contained in:
Paul Hawke 2024-12-13 08:15:13 -06:00 committed by GitHub
parent cb007608d9
commit 2c8c441f25
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
55 changed files with 1595 additions and 1835 deletions

View file

@ -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"

View file

@ -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);
}

View file

@ -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<Integer> 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);
}
}

View file

@ -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<Int> = 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
}

View file

@ -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<Contribution>()
var contributionsList = mutableListOf<Contribution>()
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<Contribution?> ->
) { list: PagedList<Contribution> ->
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))
},
{},
)
}
}

View file

@ -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[]{};
}
}
}

View file

@ -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<String> {
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()
}
}
}

View file

@ -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;
}
}
}

View file

@ -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
}
}

View file

@ -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<File> 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<File> 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;
}
}

View file

@ -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<File> {
if (file == null) return emptyList()
val buffer = ByteArray(chunkSize)
FileInputStream(file).use { fis ->
BufferedInputStream(fis).use { bis ->
val buffers: MutableList<File> = 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
}
}

View file

@ -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<Integer> 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<Integer> 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<Integer> 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<Integer> checkEXIF(String filepath) {
return EXIFReader.processMetadata(filepath);
}
/**
* Checks item caption - empty caption - existing caption
*
* @param uploadItem
* @return
*/
private Single<Integer> validateItemTitle(UploadItem uploadItem) {
Timber.d("Checking for image title %s", uploadItem.getUploadMediaDetails());
List<UploadMediaDetail> 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<Integer> 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<Integer> 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<Integer> 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());
}
}

View file

@ -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<Int> {
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<Int> {
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<Int> =
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<Int> =
EXIFReader.processMetadata(filepath)
/**
* Checks item caption - empty caption - existing caption
*/
private fun validateItemTitle(uploadItem: UploadItem): Single<Int> {
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<Int> {
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<Int> {
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<Int> {
Timber.d("Checking for image geolocation %s", filePath)
if (place == null || StringUtils.isBlank(place.wikiDataEntityId)) {
return Single.just(IMAGE_OK)
}
return Single.fromCallable<String?> { filePath }
.flatMap { path: String? ->
Single.just<String?>(
fileUtilsWrapper.getGeolocationOfFile(path!!, inAppPictureLocation)
)
}
.flatMap { geoLocation: String? ->
if (geoLocation.isNullOrBlank()) {
return@flatMap Single.just<Int>(IMAGE_OK)
}
imageUtilsWrapper.checkImageGeolocationIsDifferent(geoLocation, place.getLocation())
}
.subscribeOn(Schedulers.io())
}
}

View file

@ -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<String> 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);
}
}

View file

@ -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"
}
}

View file

@ -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<fr.free.nrw.commons.upload.PendingUploadsContract.View> {
/**
* Deletes a upload.
*/
void deleteUpload(Contribution contribution, Context context);
}
}

View file

@ -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<View> {
/**
* Deletes a upload.
*/
fun deleteUpload(contribution: Contribution?, context: Context?)
}
}

View file

@ -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<Contribution>()
private var contributionsList = mutableListOf<Contribution>()
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<Contribution?> ->
pendingUploadsPresenter.totalContributionList
.observe(viewLifecycleOwner) { list: PagedList<Contribution> ->
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,
),
)
},

View file

@ -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<PagedList<Contribution>> totalContributionList;
LiveData<PagedList<Contribution>> 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<Integer, Contribution> factory;
factory = contributionsRepository.fetchContributionsWithStatesSortedByDateUploadStarted(
Arrays.asList(Contribution.STATE_QUEUED, Contribution.STATE_IN_PROGRESS,
Contribution.STATE_PAUSED));
LivePagedListBuilder livePagedListBuilder = new LivePagedListBuilder(factory,
pagedListConfig);
totalContributionList = livePagedListBuilder.build();
}
/**
* Setups the paged list of Failed Uploads. This method sets the configuration for paged list
* and ties it up with the live data object. This method can be tweaked to update the lazy
* loading behavior of the contributions list
*/
void getFailedContributions() {
final PagedList.Config pagedListConfig =
(new PagedList.Config.Builder())
.setPrefetchDistance(50)
.setPageSize(10).build();
Factory<Integer, Contribution> factory;
factory = contributionsRepository.fetchContributionsWithStatesSortedByDateUploadStarted(
Collections.singletonList(Contribution.STATE_FAILED));
LivePagedListBuilder livePagedListBuilder = new LivePagedListBuilder(factory,
pagedListConfig);
failedContributionList = livePagedListBuilder.build();
}
@Override
public void onAttachView(@NonNull View view) {
}
@Override
public void onDetachView() {
compositeDisposable.clear();
contributionsRemoteDataSource.dispose();
contributionBoundaryCallback.dispose();
}
/**
* Deletes the specified upload (contribution) from the database.
*
* @param contribution The contribution object representing the upload to be deleted.
* @param context The context in which the operation is being performed.
*/
@Override
public void deleteUpload(final Contribution contribution, Context context) {
compositeDisposable.add(contributionsRepository
.deleteContributionFromDB(contribution)
.subscribeOn(ioThreadScheduler)
.subscribe());
}
/**
* Pauses all the uploads by changing the state of contributions from STATE_QUEUED and
* STATE_IN_PROGRESS to STATE_PAUSED in the database.
*/
public void pauseUploads() {
CommonsApplication.isPaused = true;
compositeDisposable.add(contributionsRepository
.updateContributionsWithStates(
List.of(Contribution.STATE_QUEUED, Contribution.STATE_IN_PROGRESS),
Contribution.STATE_PAUSED)
.subscribeOn(ioThreadScheduler)
.subscribe());
}
/**
* Deletes contributions from the database that match the specified states.
*
* @param states A list of integers representing the states of the contributions to be deleted.
*/
public void deleteUploads(List<Integer> states) {
compositeDisposable.add(contributionsRepository
.deleteContributionsFromDBWithStates(states)
.subscribeOn(ioThreadScheduler)
.subscribe());
}
/**
* Restarts the uploads for the specified list of contributions starting from the given index.
*
* @param contributionList The list of contributions to be restarted.
* @param index The starting index in the list from which to restart uploads.
* @param context The context in which the operation is being performed.
*/
public void restartUploads(List<Contribution> contributionList, int index, Context context) {
CommonsApplication.isPaused = false;
if (index >= contributionList.size()) {
return;
}
Contribution it = contributionList.get(index);
if (it.getState() == Contribution.STATE_FAILED) {
it.setDateUploadStarted(Calendar.getInstance().getTime());
if (it.getErrorInfo() == null) {
it.setChunkInfo(null);
it.setTransferred(0);
}
compositeDisposable.add(uploadRepository
.checkDuplicateImage(it.getLocalUriPath().getPath())
.subscribeOn(ioThreadScheduler)
.subscribe(imageCheckResult -> {
if (imageCheckResult == IMAGE_OK) {
it.setState(Contribution.STATE_QUEUED);
compositeDisposable.add(contributionsRepository
.save(it)
.subscribeOn(ioThreadScheduler)
.doOnComplete(() -> {
restartUploads(contributionList, index + 1, context);
})
.subscribe(() -> WorkRequestHelper.Companion.makeOneTimeWorkRequest(
context, ExistingWorkPolicy.KEEP)));
} else {
Timber.e("Contribution already exists");
compositeDisposable.add(contributionsRepository
.deleteContributionFromDB(it)
.subscribeOn(ioThreadScheduler).doOnComplete(() -> {
restartUploads(contributionList, index + 1, context);
})
.subscribe());
}
}, throwable -> {
Timber.e(throwable);
restartUploads(contributionList, index + 1, context);
}));
} else {
it.setState(Contribution.STATE_QUEUED);
compositeDisposable.add(contributionsRepository
.save(it)
.subscribeOn(ioThreadScheduler)
.doOnComplete(() -> {
restartUploads(contributionList, index + 1, context);
})
.subscribe(() -> WorkRequestHelper.Companion.makeOneTimeWorkRequest(
context, ExistingWorkPolicy.KEEP)));
}
}
/**
* Restarts the upload for the specified list of contributions for the given index.
*
* @param contributionList The list of contributions.
* @param index The index in the list which to be restarted.
* @param context The context in which the operation is being performed.
*/
public void restartUpload(List<Contribution> contributionList, int index, Context context) {
CommonsApplication.isPaused = false;
if (index >= contributionList.size()) {
return;
}
Contribution it = contributionList.get(index);
if (it.getState() == Contribution.STATE_FAILED) {
it.setDateUploadStarted(Calendar.getInstance().getTime());
if (it.getErrorInfo() == null) {
it.setChunkInfo(null);
it.setTransferred(0);
}
compositeDisposable.add(uploadRepository
.checkDuplicateImage(it.getLocalUriPath().getPath())
.subscribeOn(ioThreadScheduler)
.subscribe(imageCheckResult -> {
if (imageCheckResult == IMAGE_OK) {
it.setState(Contribution.STATE_QUEUED);
compositeDisposable.add(contributionsRepository
.save(it)
.subscribeOn(ioThreadScheduler)
.subscribe(() -> WorkRequestHelper.Companion.makeOneTimeWorkRequest(
context, ExistingWorkPolicy.KEEP)));
} else {
Timber.e("Contribution already exists");
compositeDisposable.add(contributionsRepository
.deleteContributionFromDB(it)
.subscribeOn(ioThreadScheduler)
.subscribe());
}
}));
} else {
it.setState(Contribution.STATE_QUEUED);
compositeDisposable.add(contributionsRepository
.save(it)
.subscribeOn(ioThreadScheduler)
.subscribe(() -> WorkRequestHelper.Companion.makeOneTimeWorkRequest(
context, ExistingWorkPolicy.KEEP)));
}
}
}

View file

@ -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<PagedList<Contribution>>
lateinit var failedContributionList: LiveData<PagedList<Contribution>>
/**
* 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<Int>) {
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<Contribution>, 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<Contribution>, 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)
}
)
}
}
}

View file

@ -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<Integer> 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;
});
}
}

View file

@ -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<Int> = 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
}
}

View file

@ -1,6 +0,0 @@
package fr.free.nrw.commons.upload;
public interface SimilarImageInterface {
void showSimilarImageFragment(String originalFilePath, String possibleFilePath,
ImageCoordinates similarImageCoordinates);
}

View file

@ -0,0 +1,9 @@
package fr.free.nrw.commons.upload
interface SimilarImageInterface {
fun showSimilarImageFragment(
originalFilePath: String?,
possibleFilePath: String?,
similarImageCoordinates: ImageCoordinates?
)
}

View file

@ -1,7 +0,0 @@
package fr.free.nrw.commons.upload;
import fr.free.nrw.commons.filepicker.UploadableFile;
public interface ThumbnailClickedListener {
void thumbnailClicked(UploadableFile content);
}

View file

@ -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<Fragment> fragmentList = getSupportFragmentManager().getFragments();
final List<Fragment> 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<fragments.size();i++){
fragments.get(i).setCallback(new Callback() {
@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();
@ -598,7 +599,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();
@ -607,7 +608,7 @@ public class UploadActivity extends BaseActivity implements UploadContract.View,
}
}
@Override
public void showProgress(boolean shouldShow) {
public void showProgress(final boolean shouldShow) {
if (shouldShow) {
if (!progressDialog.isShowing()) {
progressDialog.show();
@ -619,7 +620,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);
}
@ -648,10 +649,9 @@ public class UploadActivity extends BaseActivity implements UploadContract.View,
* Users may uncheck Location tag from the Manage EXIF tags setting any time.
* So, their location must not be shared in this case.
*
* @return
*/
private boolean isLocationTagUncheckedInTheSettings() {
Set<String> prefExifTags = defaultKvStore.getStringSet(Prefs.MANAGED_EXIF_TAGS);
final Set<String> 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<UploadBaseFragment> fragments;
public UploadImageAdapter(FragmentManager fragmentManager) {
public UploadImageAdapter(final FragmentManager fragmentManager) {
super(fragmentManager);
this.fragments = new ArrayList<>();
}
public void setFragments(List<UploadBaseFragment> fragments) {
public void setFragments(final List<UploadBaseFragment> 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

View file

@ -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<ChunkInfo?>()
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,

View file

@ -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: <a href="https://github.com/commons-app/apps-android-commons/issues/5846">Issue</a>
*/
void goToUploadProgressActivity();
void askUserToLogIn();
/**
* Changes current image when one image upload is cancelled, to highlight next image in the top thumbnail.
* Fixes: <a href="https://github.com/commons-app/apps-android-commons/issues/5511">Issue</a>
*
* @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: <a href="https://github.com/commons-app/apps-android-commons/issues/5511">Issue</a>
*
* @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<UploadableFile> getUploadableFiles();
void showHideTopCard(boolean shouldShow);
void onUploadMediaDeleted(int index);
void updateTopCardTitle();
void makeUploadRequest();
}
public interface UserActionListener extends BasePresenter<View> {
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);
}
}

View file

@ -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<UploadableFile>?
fun showHideTopCard(shouldShow: Boolean)
fun onUploadMediaDeleted(index: Int)
fun updateTopCardTitle()
fun makeUploadRequest()
}
interface UserActionListener : BasePresenter<View> {
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)
}
}

View file

@ -187,7 +187,7 @@ public class UploadModel {
public Observable<Contribution> 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);

View file

@ -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);
}
}

View file

@ -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
}

View file

@ -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<UploadItem> 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<String> 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();
}

View file

@ -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<CategoryItem> categories);
void goToNextScreen();
void showNoCategorySelected();
/**
* Gets existing category names from media
*/
List<String> 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<View> {
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<List<CategoryItem>> getCategories();
void selectCategories();
}
}

View file

@ -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<CategoryItem>?)
fun goToNextScreen()
fun showNoCategorySelected()
/**
* Gets existing category names from media
*/
fun getExistingCategories(): List<String>?
/**
* 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<View> {
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<List<CategoryItem>>
fun selectCategories()
}
}

View file

@ -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<String> =
@ -244,7 +243,7 @@ class CategoriesPresenter
compositeDisposable.add(
categoryEditHelper
.makeCategoryEdit(
view.fragmentContext,
view.getFragmentContext(),
media,
selectedCategories,
wikiText,

View file

@ -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<CategoryItem> categories) {
public void setCategories(final List<CategoryItem> 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
);

View file

@ -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<DepictedItem> depictedItemList);
fun setDepictsList(depictedItemList: List<DepictedItem>)
/**
* Returns required context
*/
Context getFragmentContext();
fun getFragmentContext(): Context
/**
* Returns to previous fragment
*/
void goBackToPreviousScreen();
fun goBackToPreviousScreen()
/**
* Gets existing depictions IDs from media
*/
List<String> getExistingDepictions();
fun getExistingDepictions(): List<String>?
/**
* 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<View> {
interface UserActionListener : BasePresenter<View> {
/**
* 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<List<DepictedItem>> getDepictedItems();
fun getDepictedItems(): LiveData<List<DepictedItem>>
/**
* 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)
}
}

View file

@ -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;
}

View file

@ -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<String> =
@ -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)

View file

@ -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<String> licenses);
void setSelectedLicense(String license);
void updateLicenseSummary(String selectedLicense, int numberOfItems);
}
interface UserActionListener extends BasePresenter<View> {
void getLicenses();
void selectLicense(String licenseName);
boolean isWLMSupportedForThisPlace();
}
}

View file

@ -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<String>?)
fun setSelectedLicense(license: String?)
fun updateLicenseSummary(selectedLicense: String?, numberOfItems: Int?)
}
interface UserActionListener : BasePresenter<View> {
fun getLicenses()
fun selectLicense(licenseName: String)
fun isWLMSupportedForThisPlace(): Boolean
}
}

View file

@ -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 = "<a href='" + Utils.licenseUrlFor(licenseSummary) + "'>" +
getString(Utils.licenseNameFor(licenseSummary)) + "</a><br>";

View file

@ -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<String> licenses = repository.getLicenses();
final List<String> 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());
}

View file

@ -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<UploadMediaDetail> uploadMediaDetails);
void displayAddLocationDialog(Runnable runnable);
}
interface UserActionListener extends BasePresenter<View> {
void receiveImage(UploadableFile uploadableFile, Place place, LatLng inAppPictureLocation);
void setUploadMediaDetails(List<UploadMediaDetail> 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);
}
}

View file

@ -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<UploadMediaDetail?>?)
fun displayAddLocationDialog(runnable: Runnable?)
}
interface UserActionListener : BasePresenter<View?> {
fun receiveImage(
uploadableFile: UploadableFile?,
place: Place?,
inAppPictureLocation: LatLng?
)
fun setUploadMediaDetails(
uploadMediaDetails: List<UploadMediaDetail?>?,
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?)
}
}

View file

@ -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<UploadMediaDetail> uploadMediaDetails, int uploadItemIndex) {
public void setUploadMediaDetails(final List<UploadMediaDetail> 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<UploadItem> 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<UploadMediaDetail> deepCopy(List<UploadMediaDetail> uploadMediaDetails) {
private List<UploadMediaDetail> deepCopy(final List<UploadMediaDetail> uploadMediaDetails) {
final ArrayList<UploadMediaDetail> 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<UploadItem> uploads = repository.getUploads();
for (UploadItem uploadItem : uploads) {
for (final UploadItem uploadItem : uploads) {
uploadItem.setPlace(place);
final List<UploadMediaDetail> 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<UploadItem> 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
);

View file

@ -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);
}

View file

@ -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"

View file

@ -0,0 +1,5 @@
package fr.free.nrw.commons.utils
fun interface TimeProvider {
fun currentTimeMillis(): Long
}

View file

@ -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

View file

@ -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

View file

@ -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()
}

View file

@ -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

View file

@ -258,7 +258,7 @@ class DepictsFragmentUnitTests {
@Test
@Throws(Exception::class)
fun testGetFragmentContext() {
fragment.fragmentContext
fragment.getFragmentContext()
}
@Test