mirror of
https://github.com/commons-app/apps-android-commons.git
synced 2025-10-26 20:33:53 +01:00
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:
parent
cb007608d9
commit
2c8c441f25
55 changed files with 1595 additions and 1835 deletions
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
31
app/src/main/java/fr/free/nrw/commons/upload/EXIFReader.kt
Normal file
31
app/src/main/java/fr/free/nrw/commons/upload/EXIFReader.kt
Normal 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
|
||||
}
|
||||
|
||||
|
|
@ -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))
|
||||
},
|
||||
{},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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[]{};
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
159
app/src/main/java/fr/free/nrw/commons/upload/FileUtils.kt
Normal file
159
app/src/main/java/fr/free/nrw/commons/upload/FileUtils.kt
Normal 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
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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?)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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)));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
45
app/src/main/java/fr/free/nrw/commons/upload/ReadFBMD.kt
Normal file
45
app/src/main/java/fr/free/nrw/commons/upload/ReadFBMD.kt
Normal 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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
package fr.free.nrw.commons.upload;
|
||||
|
||||
public interface SimilarImageInterface {
|
||||
void showSimilarImageFragment(String originalFilePath, String possibleFilePath,
|
||||
ImageCoordinates similarImageCoordinates);
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package fr.free.nrw.commons.upload
|
||||
|
||||
interface SimilarImageInterface {
|
||||
fun showSimilarImageFragment(
|
||||
originalFilePath: String?,
|
||||
possibleFilePath: String?,
|
||||
similarImageCoordinates: ImageCoordinates?
|
||||
)
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
package fr.free.nrw.commons.upload;
|
||||
|
||||
import fr.free.nrw.commons.filepicker.UploadableFile;
|
||||
|
||||
public interface ThumbnailClickedListener {
|
||||
void thumbnailClicked(UploadableFile content);
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
33
app/src/main/java/fr/free/nrw/commons/upload/UploadModule.kt
Normal file
33
app/src/main/java/fr/free/nrw/commons/upload/UploadModule.kt
Normal 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
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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>";
|
||||
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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?)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
package fr.free.nrw.commons.utils
|
||||
|
||||
fun interface TimeProvider {
|
||||
fun currentTimeMillis(): Long
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -258,7 +258,7 @@ class DepictsFragmentUnitTests {
|
|||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun testGetFragmentContext() {
|
||||
fragment.fragmentContext
|
||||
fragment.getFragmentContext()
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue