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.UploadController
|
||||||
import fr.free.nrw.commons.upload.depicts.DepictsDao
|
import fr.free.nrw.commons.upload.depicts.DepictsDao
|
||||||
import fr.free.nrw.commons.utils.ConfigUtils.isBetaFlavour
|
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.WikidataEditListener
|
||||||
import fr.free.nrw.commons.wikidata.WikidataEditListenerImpl
|
import fr.free.nrw.commons.wikidata.WikidataEditListenerImpl
|
||||||
import io.reactivex.Scheduler
|
import io.reactivex.Scheduler
|
||||||
|
|
@ -224,6 +225,11 @@ open class CommonsApplicationModule(private val applicationContext: Context) {
|
||||||
fun providesContentResolver(context: Context): ContentResolver =
|
fun providesContentResolver(context: Context): ContentResolver =
|
||||||
context.contentResolver
|
context.contentResolver
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
fun provideTimeProvider(): TimeProvider {
|
||||||
|
return TimeProvider(System::currentTimeMillis)
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val IO_THREAD: String = "io_thread"
|
const val IO_THREAD: String = "io_thread"
|
||||||
const val MAIN_THREAD: String = "main_thread"
|
const val MAIN_THREAD: String = "main_thread"
|
||||||
|
|
|
||||||
|
|
@ -528,7 +528,9 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
|
||||||
final Bundle bundle = new Bundle();
|
final Bundle bundle = new Bundle();
|
||||||
try {
|
try {
|
||||||
bundle.putString("query",
|
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) {
|
} catch (IOException e) {
|
||||||
Timber.e(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.R
|
||||||
import fr.free.nrw.commons.auth.SessionManager
|
import fr.free.nrw.commons.auth.SessionManager
|
||||||
import fr.free.nrw.commons.contributions.Contribution
|
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.databinding.FragmentFailedUploadsBinding
|
||||||
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment
|
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment
|
||||||
import fr.free.nrw.commons.media.MediaClient
|
import fr.free.nrw.commons.media.MediaClient
|
||||||
|
|
@ -43,7 +44,7 @@ class FailedUploadsFragment :
|
||||||
|
|
||||||
private lateinit var adapter: FailedUploadsAdapter
|
private lateinit var adapter: FailedUploadsAdapter
|
||||||
|
|
||||||
var contributionsList = ArrayList<Contribution>()
|
var contributionsList = mutableListOf<Contribution>()
|
||||||
|
|
||||||
private lateinit var uploadProgressActivity: UploadProgressActivity
|
private lateinit var uploadProgressActivity: UploadProgressActivity
|
||||||
|
|
||||||
|
|
@ -71,7 +72,7 @@ class FailedUploadsFragment :
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
container: ViewGroup?,
|
container: ViewGroup?,
|
||||||
savedInstanceState: Bundle?,
|
savedInstanceState: Bundle?,
|
||||||
): View? {
|
): View {
|
||||||
binding = FragmentFailedUploadsBinding.inflate(layoutInflater)
|
binding = FragmentFailedUploadsBinding.inflate(layoutInflater)
|
||||||
pendingUploadsPresenter.onAttachView(this)
|
pendingUploadsPresenter.onAttachView(this)
|
||||||
initAdapter()
|
initAdapter()
|
||||||
|
|
@ -99,9 +100,9 @@ class FailedUploadsFragment :
|
||||||
pendingUploadsPresenter.getFailedContributions()
|
pendingUploadsPresenter.getFailedContributions()
|
||||||
pendingUploadsPresenter.failedContributionList.observe(
|
pendingUploadsPresenter.failedContributionList.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner,
|
||||||
) { list: PagedList<Contribution?> ->
|
) { list: PagedList<Contribution> ->
|
||||||
adapter.submitList(list)
|
adapter.submitList(list)
|
||||||
contributionsList = ArrayList()
|
contributionsList = mutableListOf()
|
||||||
list.forEach {
|
list.forEach {
|
||||||
if (it != null) {
|
if (it != null) {
|
||||||
contributionsList.add(it)
|
contributionsList.add(it)
|
||||||
|
|
@ -124,26 +125,22 @@ class FailedUploadsFragment :
|
||||||
* Restarts all the failed uploads.
|
* Restarts all the failed uploads.
|
||||||
*/
|
*/
|
||||||
fun restartUploads() {
|
fun restartUploads() {
|
||||||
if (contributionsList != null) {
|
pendingUploadsPresenter.restartUploads(
|
||||||
pendingUploadsPresenter.restartUploads(
|
contributionsList,
|
||||||
contributionsList,
|
0,
|
||||||
0,
|
requireContext().applicationContext,
|
||||||
this.requireContext().applicationContext,
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Restarts a specific upload.
|
* Restarts a specific upload.
|
||||||
*/
|
*/
|
||||||
override fun restartUpload(index: Int) {
|
override fun restartUpload(index: Int) {
|
||||||
if (contributionsList != null) {
|
pendingUploadsPresenter.restartUpload(
|
||||||
pendingUploadsPresenter.restartUpload(
|
contributionsList,
|
||||||
contributionsList,
|
index,
|
||||||
index,
|
requireContext().applicationContext,
|
||||||
this.requireContext().applicationContext,
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -166,7 +163,7 @@ class FailedUploadsFragment :
|
||||||
ViewUtil.showShortToast(context, R.string.cancelling_upload)
|
ViewUtil.showShortToast(context, R.string.cancelling_upload)
|
||||||
pendingUploadsPresenter.deleteUpload(
|
pendingUploadsPresenter.deleteUpload(
|
||||||
contribution,
|
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.
|
* Deletes all the uploads after getting a confirmation from the user using Dialog.
|
||||||
*/
|
*/
|
||||||
fun deleteUploads() {
|
fun deleteUploads() {
|
||||||
if (contributionsList != null) {
|
DialogUtil.showAlertDialog(
|
||||||
DialogUtil.showAlertDialog(
|
requireActivity(),
|
||||||
requireActivity(),
|
String.format(
|
||||||
String.format(
|
Locale.getDefault(),
|
||||||
Locale.getDefault(),
|
requireActivity().getString(R.string.cancelling_all_the_uploads),
|
||||||
requireActivity().getString(R.string.cancelling_all_the_uploads),
|
),
|
||||||
),
|
String.format(
|
||||||
String.format(
|
Locale.getDefault(),
|
||||||
Locale.getDefault(),
|
requireActivity().getString(R.string.are_you_sure_that_you_want_cancel_all_the_uploads),
|
||||||
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.yes)),
|
String.format(Locale.getDefault(), requireActivity().getString(R.string.no)),
|
||||||
String.format(Locale.getDefault(), requireActivity().getString(R.string.no)),
|
{
|
||||||
{
|
ViewUtil.showShortToast(context, R.string.cancelling_upload)
|
||||||
ViewUtil.showShortToast(context, R.string.cancelling_upload)
|
uploadProgressActivity.hidePendingIcons()
|
||||||
uploadProgressActivity.hidePendingIcons()
|
pendingUploadsPresenter.deleteUploads(listOf(STATE_FAILED))
|
||||||
pendingUploadsPresenter.deleteUploads(
|
},
|
||||||
listOf(Contribution.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.CommonsApplication
|
||||||
import fr.free.nrw.commons.R
|
import fr.free.nrw.commons.R
|
||||||
import fr.free.nrw.commons.contributions.Contribution
|
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.databinding.FragmentPendingUploadsBinding
|
||||||
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment
|
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment
|
||||||
import fr.free.nrw.commons.utils.DialogUtil.showAlertDialog
|
import fr.free.nrw.commons.utils.DialogUtil.showAlertDialog
|
||||||
|
|
@ -35,7 +38,8 @@ class PendingUploadsFragment :
|
||||||
private lateinit var adapter: PendingUploadsAdapter
|
private lateinit var adapter: PendingUploadsAdapter
|
||||||
|
|
||||||
private var contributionsSize = 0
|
private var contributionsSize = 0
|
||||||
var contributionsList = ArrayList<Contribution>()
|
|
||||||
|
private var contributionsList = mutableListOf<Contribution>()
|
||||||
|
|
||||||
override fun onAttach(context: Context) {
|
override fun onAttach(context: Context) {
|
||||||
super.onAttach(context)
|
super.onAttach(context)
|
||||||
|
|
@ -48,7 +52,7 @@ class PendingUploadsFragment :
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
container: ViewGroup?,
|
container: ViewGroup?,
|
||||||
savedInstanceState: Bundle?,
|
savedInstanceState: Bundle?,
|
||||||
): View? {
|
): View {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
binding = FragmentPendingUploadsBinding.inflate(inflater, container, false)
|
binding = FragmentPendingUploadsBinding.inflate(inflater, container, false)
|
||||||
pendingUploadsPresenter.onAttachView(this)
|
pendingUploadsPresenter.onAttachView(this)
|
||||||
|
|
@ -71,27 +75,24 @@ class PendingUploadsFragment :
|
||||||
/**
|
/**
|
||||||
* Initializes the recycler view.
|
* Initializes the recycler view.
|
||||||
*/
|
*/
|
||||||
fun initRecyclerView() {
|
private fun initRecyclerView() {
|
||||||
binding.pendingUploadsRecyclerView.setLayoutManager(LinearLayoutManager(this.context))
|
binding.pendingUploadsRecyclerView.setLayoutManager(LinearLayoutManager(this.context))
|
||||||
binding.pendingUploadsRecyclerView.adapter = adapter
|
binding.pendingUploadsRecyclerView.adapter = adapter
|
||||||
pendingUploadsPresenter.setup()
|
pendingUploadsPresenter.setup()
|
||||||
pendingUploadsPresenter.totalContributionList.observe(
|
pendingUploadsPresenter.totalContributionList
|
||||||
viewLifecycleOwner,
|
.observe(viewLifecycleOwner) { list: PagedList<Contribution> ->
|
||||||
) { list: PagedList<Contribution?> ->
|
|
||||||
contributionsSize = list.size
|
contributionsSize = list.size
|
||||||
contributionsList = ArrayList()
|
contributionsList = mutableListOf()
|
||||||
var pausedOrQueuedUploads = 0
|
var pausedOrQueuedUploads = 0
|
||||||
list.forEach {
|
list.forEach {
|
||||||
if (it != null) {
|
if (it != null) {
|
||||||
if (it.state == Contribution.STATE_PAUSED ||
|
if (it.state == STATE_PAUSED ||
|
||||||
it.state == Contribution.STATE_QUEUED ||
|
it.state == STATE_QUEUED ||
|
||||||
it.state == Contribution.STATE_IN_PROGRESS
|
it.state == STATE_IN_PROGRESS
|
||||||
) {
|
) {
|
||||||
contributionsList.add(it)
|
contributionsList.add(it)
|
||||||
}
|
}
|
||||||
if (it.state == Contribution.STATE_PAUSED ||
|
if (it.state == STATE_PAUSED || it.state == STATE_QUEUED) {
|
||||||
it.state == Contribution.STATE_QUEUED
|
|
||||||
) {
|
|
||||||
pausedOrQueuedUploads++
|
pausedOrQueuedUploads++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -104,7 +105,7 @@ class PendingUploadsFragment :
|
||||||
binding.nopendingTextView.visibility = View.GONE
|
binding.nopendingTextView.visibility = View.GONE
|
||||||
binding.pendingUplaodsLl.visibility = View.VISIBLE
|
binding.pendingUplaodsLl.visibility = View.VISIBLE
|
||||||
adapter.submitList(list)
|
adapter.submitList(list)
|
||||||
binding.progressTextView.setText(contributionsSize.toString() + " uploads left")
|
binding.progressTextView.setText("$contributionsSize uploads left")
|
||||||
if ((pausedOrQueuedUploads == contributionsSize) || CommonsApplication.isPaused) {
|
if ((pausedOrQueuedUploads == contributionsSize) || CommonsApplication.isPaused) {
|
||||||
uploadProgressActivity.setPausedIcon(true)
|
uploadProgressActivity.setPausedIcon(true)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -118,23 +119,18 @@ class PendingUploadsFragment :
|
||||||
* Cancels a specific upload after getting a confirmation from the user using Dialog.
|
* Cancels a specific upload after getting a confirmation from the user using Dialog.
|
||||||
*/
|
*/
|
||||||
override fun deleteUpload(contribution: Contribution?) {
|
override fun deleteUpload(contribution: Contribution?) {
|
||||||
|
val activity = requireActivity()
|
||||||
|
val locale = Locale.getDefault()
|
||||||
showAlertDialog(
|
showAlertDialog(
|
||||||
requireActivity(),
|
activity,
|
||||||
String.format(
|
String.format(locale, activity.getString(R.string.cancelling_upload)),
|
||||||
Locale.getDefault(),
|
String.format(locale, activity.getString(R.string.cancel_upload_dialog)),
|
||||||
requireActivity().getString(R.string.cancelling_upload),
|
String.format(locale, activity.getString(R.string.yes)),
|
||||||
),
|
String.format(locale, activity.getString(R.string.no)),
|
||||||
String.format(
|
|
||||||
Locale.getDefault(),
|
|
||||||
requireActivity().getString(R.string.cancel_upload_dialog),
|
|
||||||
),
|
|
||||||
String.format(Locale.getDefault(), requireActivity().getString(R.string.yes)),
|
|
||||||
String.format(Locale.getDefault(), requireActivity().getString(R.string.no)),
|
|
||||||
{
|
{
|
||||||
ViewUtil.showShortToast(context, R.string.cancelling_upload)
|
ViewUtil.showShortToast(context, R.string.cancelling_upload)
|
||||||
pendingUploadsPresenter.deleteUpload(
|
pendingUploadsPresenter.deleteUpload(
|
||||||
contribution,
|
contribution, requireContext().applicationContext,
|
||||||
this.requireContext().applicationContext,
|
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{},
|
{},
|
||||||
|
|
@ -144,47 +140,35 @@ class PendingUploadsFragment :
|
||||||
/**
|
/**
|
||||||
* Restarts all the paused uploads.
|
* Restarts all the paused uploads.
|
||||||
*/
|
*/
|
||||||
fun restartUploads() {
|
fun restartUploads() = pendingUploadsPresenter.restartUploads(
|
||||||
if (contributionsList != null) {
|
contributionsList, 0, requireContext().applicationContext
|
||||||
pendingUploadsPresenter.restartUploads(
|
)
|
||||||
contributionsList,
|
|
||||||
0,
|
|
||||||
this.requireContext().applicationContext,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pauses all the ongoing uploads.
|
* Pauses all the ongoing uploads.
|
||||||
*/
|
*/
|
||||||
fun pauseUploads() {
|
fun pauseUploads() = pendingUploadsPresenter.pauseUploads()
|
||||||
pendingUploadsPresenter.pauseUploads()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cancels all the uploads after getting a confirmation from the user using Dialog.
|
* Cancels all the uploads after getting a confirmation from the user using Dialog.
|
||||||
*/
|
*/
|
||||||
fun deleteUploads() {
|
fun deleteUploads() {
|
||||||
|
val activity = requireActivity()
|
||||||
|
val locale = Locale.getDefault()
|
||||||
showAlertDialog(
|
showAlertDialog(
|
||||||
requireActivity(),
|
activity,
|
||||||
String.format(
|
String.format(locale, activity.getString(R.string.cancelling_all_the_uploads)),
|
||||||
Locale.getDefault(),
|
String.format(locale, activity.getString(R.string.are_you_sure_that_you_want_cancel_all_the_uploads)),
|
||||||
requireActivity().getString(R.string.cancelling_all_the_uploads),
|
String.format(locale, activity.getString(R.string.yes)),
|
||||||
),
|
String.format(locale, activity.getString(R.string.no)),
|
||||||
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)
|
ViewUtil.showShortToast(context, R.string.cancelling_upload)
|
||||||
uploadProgressActivity.hidePendingIcons()
|
uploadProgressActivity.hidePendingIcons()
|
||||||
pendingUploadsPresenter.deleteUploads(
|
pendingUploadsPresenter.deleteUploads(
|
||||||
listOf(
|
listOf(
|
||||||
Contribution.STATE_QUEUED,
|
STATE_QUEUED,
|
||||||
Contribution.STATE_IN_PROGRESS,
|
STATE_IN_PROGRESS,
|
||||||
Contribution.STATE_PAUSED,
|
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 javax.inject.Named;
|
||||||
import timber.log.Timber;
|
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
|
@Inject
|
||||||
ContributionController contributionController;
|
ContributionController contributionController;
|
||||||
|
|
@ -148,7 +149,7 @@ public class UploadActivity extends BaseActivity implements UploadContract.View,
|
||||||
|
|
||||||
@SuppressLint("CheckResult")
|
@SuppressLint("CheckResult")
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(final Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
binding = ActivityUploadBinding.inflate(getLayoutInflater());
|
binding = ActivityUploadBinding.inflate(getLayoutInflater());
|
||||||
|
|
@ -160,9 +161,9 @@ public class UploadActivity extends BaseActivity implements UploadContract.View,
|
||||||
*/
|
*/
|
||||||
if (savedInstanceState != null) {
|
if (savedInstanceState != null) {
|
||||||
isFragmentsSaved = true;
|
isFragmentsSaved = true;
|
||||||
List<Fragment> fragmentList = getSupportFragmentManager().getFragments();
|
final List<Fragment> fragmentList = getSupportFragmentManager().getFragments();
|
||||||
fragments = new ArrayList<>();
|
fragments = new ArrayList<>();
|
||||||
for (Fragment fragment : fragmentList) {
|
for (final Fragment fragment : fragmentList) {
|
||||||
fragments.add((UploadBaseFragment) fragment);
|
fragments.add((UploadBaseFragment) fragment);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -174,8 +175,8 @@ public class UploadActivity extends BaseActivity implements UploadContract.View,
|
||||||
nearbyPopupAnswers = new HashMap<>();
|
nearbyPopupAnswers = new HashMap<>();
|
||||||
//getting the current dpi of the device and if it is less than 320dp i.e. overlapping
|
//getting the current dpi of the device and if it is less than 320dp i.e. overlapping
|
||||||
//threshold, thumbnails automatically minimizes
|
//threshold, thumbnails automatically minimizes
|
||||||
DisplayMetrics metrics = getResources().getDisplayMetrics();
|
final DisplayMetrics metrics = getResources().getDisplayMetrics();
|
||||||
float dpi = (metrics.widthPixels)/(metrics.density);
|
final float dpi = (metrics.widthPixels)/(metrics.density);
|
||||||
if (dpi<=321) {
|
if (dpi<=321) {
|
||||||
onRlContainerTitleClicked();
|
onRlContainerTitleClicked();
|
||||||
}
|
}
|
||||||
|
|
@ -217,13 +218,13 @@ public class UploadActivity extends BaseActivity implements UploadContract.View,
|
||||||
binding.vpUpload.setAdapter(uploadImagesAdapter);
|
binding.vpUpload.setAdapter(uploadImagesAdapter);
|
||||||
binding.vpUpload.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
|
binding.vpUpload.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onPageScrolled(int position, float positionOffset,
|
public void onPageScrolled(final int position, final float positionOffset,
|
||||||
int positionOffsetPixels) {
|
final int positionOffsetPixels) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPageSelected(int position) {
|
public void onPageSelected(final int position) {
|
||||||
currentSelectedPosition = position;
|
currentSelectedPosition = position;
|
||||||
if (position >= uploadableFiles.size()) {
|
if (position >= uploadableFiles.size()) {
|
||||||
binding.cvContainerTopCard.setVisibility(View.GONE);
|
binding.cvContainerTopCard.setVisibility(View.GONE);
|
||||||
|
|
@ -235,7 +236,7 @@ public class UploadActivity extends BaseActivity implements UploadContract.View,
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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
|
* Show/Hide the progress dialog
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void showProgress(boolean shouldShow) {
|
public void showProgress(final boolean shouldShow) {
|
||||||
if (shouldShow) {
|
if (shouldShow) {
|
||||||
if (!progressDialog.isShowing()) {
|
if (!progressDialog.isShowing()) {
|
||||||
progressDialog.show();
|
progressDialog.show();
|
||||||
|
|
@ -343,7 +344,7 @@ public class UploadActivity extends BaseActivity implements UploadContract.View,
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getIndexInViewFlipper(UploadBaseFragment fragment) {
|
public int getIndexInViewFlipper(final UploadBaseFragment fragment) {
|
||||||
return fragments.indexOf(fragment);
|
return fragments.indexOf(fragment);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -358,7 +359,7 @@ public class UploadActivity extends BaseActivity implements UploadContract.View,
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void showMessage(int messageResourceId) {
|
public void showMessage(final int messageResourceId) {
|
||||||
ViewUtil.showLongToast(this, messageResourceId);
|
ViewUtil.showLongToast(this, messageResourceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -368,12 +369,12 @@ public class UploadActivity extends BaseActivity implements UploadContract.View,
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void showHideTopCard(boolean shouldShow) {
|
public void showHideTopCard(final boolean shouldShow) {
|
||||||
binding.llContainerTopCard.setVisibility(shouldShow ? View.VISIBLE : View.GONE);
|
binding.llContainerTopCard.setVisibility(shouldShow ? View.VISIBLE : View.GONE);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onUploadMediaDeleted(int index) {
|
public void onUploadMediaDeleted(final int index) {
|
||||||
fragments.remove(index);//Remove the corresponding fragment
|
fragments.remove(index);//Remove the corresponding fragment
|
||||||
uploadableFiles.remove(index);//Remove the files from the list
|
uploadableFiles.remove(index);//Remove the files from the list
|
||||||
thumbnailsAdapter.notifyItemRemoved(index); //Notify the thumbnails adapter
|
thumbnailsAdapter.notifyItemRemoved(index); //Notify the thumbnails adapter
|
||||||
|
|
@ -396,7 +397,7 @@ public class UploadActivity extends BaseActivity implements UploadContract.View,
|
||||||
public void askUserToLogIn() {
|
public void askUserToLogIn() {
|
||||||
Timber.d("current session is null, asking user to login");
|
Timber.d("current session is null, asking user to login");
|
||||||
ViewUtil.showLongToast(this, getString(R.string.user_not_logged_in));
|
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);
|
startActivity(loginIntent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -408,10 +409,10 @@ public class UploadActivity extends BaseActivity implements UploadContract.View,
|
||||||
if (requestCode == RequestCodes.STORAGE) {
|
if (requestCode == RequestCodes.STORAGE) {
|
||||||
if (VERSION.SDK_INT >= VERSION_CODES.M) {
|
if (VERSION.SDK_INT >= VERSION_CODES.M) {
|
||||||
for (int i = 0; i < grantResults.length; i++) {
|
for (int i = 0; i < grantResults.length; i++) {
|
||||||
String permission = permissions[i];
|
final String permission = permissions[i];
|
||||||
areAllGranted = grantResults[i] == PackageManager.PERMISSION_GRANTED;
|
areAllGranted = grantResults[i] == PackageManager.PERMISSION_GRANTED;
|
||||||
if (grantResults[i] == PackageManager.PERMISSION_DENIED) {
|
if (grantResults[i] == PackageManager.PERMISSION_DENIED) {
|
||||||
boolean showRationale = shouldShowRequestPermissionRationale(permission);
|
final boolean showRationale = shouldShowRequestPermissionRationale(permission);
|
||||||
if (!showRationale) {
|
if (!showRationale) {
|
||||||
DialogUtil.showAlertDialog(this,
|
DialogUtil.showAlertDialog(this,
|
||||||
getString(R.string.storage_permissions_denied),
|
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.
|
* @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;
|
uploadIsOfAPlace = uploadOfAPlace;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void receiveSharedItems() {
|
private void receiveSharedItems() {
|
||||||
thumbnailsAdapter.context=this;
|
ThumbnailsAdapter.context=this;
|
||||||
Intent intent = getIntent();
|
final Intent intent = getIntent();
|
||||||
String action = intent.getAction();
|
final String action = intent.getAction();
|
||||||
if (Intent.ACTION_SEND.equals(action) || Intent.ACTION_SEND_MULTIPLE.equals(action)) {
|
if (Intent.ACTION_SEND.equals(action) || Intent.ACTION_SEND_MULTIPLE.equals(action)) {
|
||||||
receiveExternalSharedItems();
|
receiveExternalSharedItems();
|
||||||
} else if (ACTION_INTERNAL_UPLOADS.equals(action)) {
|
} else if (ACTION_INTERNAL_UPLOADS.equals(action)) {
|
||||||
|
|
@ -481,8 +482,8 @@ public class UploadActivity extends BaseActivity implements UploadContract.View,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
for (UploadableFile uploadableFile : uploadableFiles) {
|
for (final UploadableFile uploadableFile : uploadableFiles) {
|
||||||
UploadMediaDetailFragment uploadMediaDetailFragment = new UploadMediaDetailFragment();
|
final UploadMediaDetailFragment uploadMediaDetailFragment = new UploadMediaDetailFragment();
|
||||||
|
|
||||||
if (!uploadIsOfAPlace) {
|
if (!uploadIsOfAPlace) {
|
||||||
handleLocation();
|
handleLocation();
|
||||||
|
|
@ -492,9 +493,9 @@ public class UploadActivity extends BaseActivity implements UploadContract.View,
|
||||||
uploadMediaDetailFragment.setImageToBeUploaded(uploadableFile, place, currLocation);
|
uploadMediaDetailFragment.setImageToBeUploaded(uploadableFile, place, currLocation);
|
||||||
}
|
}
|
||||||
|
|
||||||
UploadMediaDetailFragmentCallback uploadMediaDetailFragmentCallback = new UploadMediaDetailFragmentCallback() {
|
final UploadMediaDetailFragmentCallback uploadMediaDetailFragmentCallback = new UploadMediaDetailFragmentCallback() {
|
||||||
@Override
|
@Override
|
||||||
public void deletePictureAtIndex(int index) {
|
public void deletePictureAtIndex(final int index) {
|
||||||
store.putInt(keyForCurrentUploadImagesSize,
|
store.putInt(keyForCurrentUploadImagesSize,
|
||||||
(store.getInt(keyForCurrentUploadImagesSize) - 1));
|
(store.getInt(keyForCurrentUploadImagesSize) - 1));
|
||||||
presenter.deletePictureAtIndex(index);
|
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.
|
* @param filepath The file path of the new thumbnail image.
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void changeThumbnail(int index, String filepath) {
|
public void changeThumbnail(final int index, final String filepath) {
|
||||||
uploadableFiles.remove(index);
|
uploadableFiles.remove(index);
|
||||||
uploadableFiles.add(index, new UploadableFile(new File(filepath)));
|
uploadableFiles.add(index, new UploadableFile(new File(filepath)));
|
||||||
binding.rvThumbnails.getAdapter().notifyDataSetChanged();
|
binding.rvThumbnails.getAdapter().notifyDataSetChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onNextButtonClicked(int index) {
|
public void onNextButtonClicked(final int index) {
|
||||||
UploadActivity.this.onNextButtonClicked(index);
|
UploadActivity.this.onNextButtonClicked(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPreviousButtonClicked(int index) {
|
public void onPreviousButtonClicked(final int index) {
|
||||||
UploadActivity.this.onPreviousButtonClicked(index);
|
UploadActivity.this.onPreviousButtonClicked(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void showProgress(boolean shouldShow) {
|
public void showProgress(final boolean shouldShow) {
|
||||||
UploadActivity.this.showProgress(shouldShow);
|
UploadActivity.this.showProgress(shouldShow);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getIndexInViewFlipper(UploadBaseFragment fragment) {
|
public int getIndexInViewFlipper(final UploadBaseFragment fragment) {
|
||||||
return fragments.indexOf(fragment);
|
return fragments.indexOf(fragment);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -549,7 +550,7 @@ public class UploadActivity extends BaseActivity implements UploadContract.View,
|
||||||
};
|
};
|
||||||
|
|
||||||
if(isFragmentsSaved){
|
if(isFragmentsSaved){
|
||||||
UploadMediaDetailFragment fragment = (UploadMediaDetailFragment) fragments.get(0);
|
final UploadMediaDetailFragment fragment = (UploadMediaDetailFragment) fragments.get(0);
|
||||||
fragment.setCallback(uploadMediaDetailFragmentCallback);
|
fragment.setCallback(uploadMediaDetailFragmentCallback);
|
||||||
}else{
|
}else{
|
||||||
uploadMediaDetailFragment.setCallback(uploadMediaDetailFragmentCallback);
|
uploadMediaDetailFragment.setCallback(uploadMediaDetailFragmentCallback);
|
||||||
|
|
@ -562,7 +563,7 @@ public class UploadActivity extends BaseActivity implements UploadContract.View,
|
||||||
if(!isFragmentsSaved){
|
if(!isFragmentsSaved){
|
||||||
uploadCategoriesFragment = new UploadCategoriesFragment();
|
uploadCategoriesFragment = new UploadCategoriesFragment();
|
||||||
if (place != null) {
|
if (place != null) {
|
||||||
Bundle categoryBundle = new Bundle();
|
final Bundle categoryBundle = new Bundle();
|
||||||
categoryBundle.putString(SELECTED_NEARBY_PLACE_CATEGORY, place.getCategory());
|
categoryBundle.putString(SELECTED_NEARBY_PLACE_CATEGORY, place.getCategory());
|
||||||
uploadCategoriesFragment.setArguments(categoryBundle);
|
uploadCategoriesFragment.setArguments(categoryBundle);
|
||||||
}
|
}
|
||||||
|
|
@ -570,7 +571,7 @@ public class UploadActivity extends BaseActivity implements UploadContract.View,
|
||||||
uploadCategoriesFragment.setCallback(this);
|
uploadCategoriesFragment.setCallback(this);
|
||||||
|
|
||||||
depictsFragment = new DepictsFragment();
|
depictsFragment = new DepictsFragment();
|
||||||
Bundle placeBundle = new Bundle();
|
final Bundle placeBundle = new Bundle();
|
||||||
placeBundle.putParcelable(SELECTED_NEARBY_PLACE, place);
|
placeBundle.putParcelable(SELECTED_NEARBY_PLACE, place);
|
||||||
depictsFragment.setArguments(placeBundle);
|
depictsFragment.setArguments(placeBundle);
|
||||||
depictsFragment.setCallback(this);
|
depictsFragment.setCallback(this);
|
||||||
|
|
@ -586,7 +587,7 @@ public class UploadActivity extends BaseActivity implements UploadContract.View,
|
||||||
for(int i=1;i<fragments.size();i++){
|
for(int i=1;i<fragments.size();i++){
|
||||||
fragments.get(i).setCallback(new Callback() {
|
fragments.get(i).setCallback(new Callback() {
|
||||||
@Override
|
@Override
|
||||||
public void onNextButtonClicked(int index) {
|
public void onNextButtonClicked(final int index) {
|
||||||
if (index < fragments.size() - 1) {
|
if (index < fragments.size() - 1) {
|
||||||
binding.vpUpload.setCurrentItem(index + 1, false);
|
binding.vpUpload.setCurrentItem(index + 1, false);
|
||||||
fragments.get(index + 1).onBecameVisible();
|
fragments.get(index + 1).onBecameVisible();
|
||||||
|
|
@ -598,7 +599,7 @@ public class UploadActivity extends BaseActivity implements UploadContract.View,
|
||||||
|
|
||||||
}
|
}
|
||||||
@Override
|
@Override
|
||||||
public void onPreviousButtonClicked(int index) {
|
public void onPreviousButtonClicked(final int index) {
|
||||||
if (index != 0) {
|
if (index != 0) {
|
||||||
binding.vpUpload.setCurrentItem(index - 1, true);
|
binding.vpUpload.setCurrentItem(index - 1, true);
|
||||||
fragments.get(index - 1).onBecameVisible();
|
fragments.get(index - 1).onBecameVisible();
|
||||||
|
|
@ -607,7 +608,7 @@ public class UploadActivity extends BaseActivity implements UploadContract.View,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@Override
|
@Override
|
||||||
public void showProgress(boolean shouldShow) {
|
public void showProgress(final boolean shouldShow) {
|
||||||
if (shouldShow) {
|
if (shouldShow) {
|
||||||
if (!progressDialog.isShowing()) {
|
if (!progressDialog.isShowing()) {
|
||||||
progressDialog.show();
|
progressDialog.show();
|
||||||
|
|
@ -619,7 +620,7 @@ public class UploadActivity extends BaseActivity implements UploadContract.View,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@Override
|
@Override
|
||||||
public int getIndexInViewFlipper(UploadBaseFragment fragment) {
|
public int getIndexInViewFlipper(final UploadBaseFragment fragment) {
|
||||||
return fragments.indexOf(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.
|
* Users may uncheck Location tag from the Manage EXIF tags setting any time.
|
||||||
* So, their location must not be shared in this case.
|
* So, their location must not be shared in this case.
|
||||||
*
|
*
|
||||||
* @return
|
|
||||||
*/
|
*/
|
||||||
private boolean isLocationTagUncheckedInTheSettings() {
|
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))) {
|
if (prefExifTags.contains(getString(R.string.exif_tag_location))) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -666,7 +666,7 @@ public class UploadActivity extends BaseActivity implements UploadContract.View,
|
||||||
* @param maxSize Max size of the {@code uploadableFiles}
|
* @param maxSize Max size of the {@code uploadableFiles}
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void highlightNextImageOnCancelledImage(int index, int maxSize) {
|
public void highlightNextImageOnCancelledImage(final int index, final int maxSize) {
|
||||||
if (binding.vpUpload != null && index < (maxSize)) {
|
if (binding.vpUpload != null && index < (maxSize)) {
|
||||||
binding.vpUpload.setCurrentItem(index + 1, false);
|
binding.vpUpload.setCurrentItem(index + 1, false);
|
||||||
binding.vpUpload.setCurrentItem(index, 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
|
* @param isCancelled Is true when user has cancelled upload of any image in current upload
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void setImageCancelled(boolean isCancelled) {
|
public void setImageCancelled(final boolean isCancelled) {
|
||||||
BasicKvStore basicKvStore = new BasicKvStore(this,"IsAnyImageCancelled");
|
final BasicKvStore basicKvStore = new BasicKvStore(this,"IsAnyImageCancelled");
|
||||||
basicKvStore.putBoolean("IsAnyImageCancelled", isCancelled);
|
basicKvStore.putBoolean("IsAnyImageCancelled", isCancelled);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -690,15 +690,12 @@ public class UploadActivity extends BaseActivity implements UploadContract.View,
|
||||||
* Calculate the difference between current location and
|
* Calculate the difference between current location and
|
||||||
* location recorded before capturing the image
|
* 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) {
|
if (prevLocation == null) {
|
||||||
return 0.0f;
|
return 0.0f;
|
||||||
}
|
}
|
||||||
float[] distance = new float[2];
|
final float[] distance = new float[2];
|
||||||
Location.distanceBetween(
|
Location.distanceBetween(
|
||||||
currLocation.getLatitude(), currLocation.getLongitude(),
|
currLocation.getLatitude(), currLocation.getLongitude(),
|
||||||
prevLocation.getLatitude(), prevLocation.getLongitude(), distance);
|
prevLocation.getLatitude(), prevLocation.getLongitude(), distance);
|
||||||
|
|
@ -710,7 +707,7 @@ public class UploadActivity extends BaseActivity implements UploadContract.View,
|
||||||
}
|
}
|
||||||
|
|
||||||
private void receiveInternalSharedItems() {
|
private void receiveInternalSharedItems() {
|
||||||
Intent intent = getIntent();
|
final Intent intent = getIntent();
|
||||||
|
|
||||||
Timber.d("Received intent %s with action %s", intent.toString(), intent.getAction());
|
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
|
@Override
|
||||||
public void showAlertDialog(int messageResourceId, Runnable onPositiveClick) {
|
public void showAlertDialog(final int messageResourceId, @NonNull final Runnable onPositiveClick) {
|
||||||
DialogUtil.showAlertDialog(this,
|
DialogUtil.showAlertDialog(this,
|
||||||
"",
|
"",
|
||||||
getString(messageResourceId),
|
getString(messageResourceId),
|
||||||
|
|
@ -755,7 +752,7 @@ public class UploadActivity extends BaseActivity implements UploadContract.View,
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onNextButtonClicked(int index) {
|
public void onNextButtonClicked(final int index) {
|
||||||
if (index < fragments.size() - 1) {
|
if (index < fragments.size() - 1) {
|
||||||
binding.vpUpload.setCurrentItem(index + 1, false);
|
binding.vpUpload.setCurrentItem(index + 1, false);
|
||||||
fragments.get(index + 1).onBecameVisible();
|
fragments.get(index + 1).onBecameVisible();
|
||||||
|
|
@ -771,7 +768,7 @@ public class UploadActivity extends BaseActivity implements UploadContract.View,
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPreviousButtonClicked(int index) {
|
public void onPreviousButtonClicked(final int index) {
|
||||||
if (index != 0) {
|
if (index != 0) {
|
||||||
binding.vpUpload.setCurrentItem(index - 1, true);
|
binding.vpUpload.setCurrentItem(index - 1, true);
|
||||||
fragments.get(index - 1).onBecameVisible();
|
fragments.get(index - 1).onBecameVisible();
|
||||||
|
|
@ -786,7 +783,7 @@ public class UploadActivity extends BaseActivity implements UploadContract.View,
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onThumbnailDeleted(int position) {
|
public void onThumbnailDeleted(final int position) {
|
||||||
presenter.deletePictureAtIndex(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;
|
List<UploadBaseFragment> fragments;
|
||||||
|
|
||||||
public UploadImageAdapter(FragmentManager fragmentManager) {
|
public UploadImageAdapter(final FragmentManager fragmentManager) {
|
||||||
super(fragmentManager);
|
super(fragmentManager);
|
||||||
this.fragments = new ArrayList<>();
|
this.fragments = new ArrayList<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setFragments(List<UploadBaseFragment> fragments) {
|
public void setFragments(final List<UploadBaseFragment> fragments) {
|
||||||
this.fragments = fragments;
|
this.fragments = fragments;
|
||||||
notifyDataSetChanged();
|
notifyDataSetChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public Fragment getItem(int position) {
|
public Fragment getItem(final int position) {
|
||||||
return fragments.get(position);
|
return fragments.get(position);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -819,7 +817,7 @@ public class UploadActivity extends BaseActivity implements UploadContract.View,
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getItemPosition(Object object) {
|
public int getItemPosition(@NonNull final Object item) {
|
||||||
return PagerAdapter.POSITION_NONE;
|
return PagerAdapter.POSITION_NONE;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -893,11 +891,11 @@ public class UploadActivity extends BaseActivity implements UploadContract.View,
|
||||||
private void showAlertDialogForCategories() {
|
private void showAlertDialogForCategories() {
|
||||||
UploadMediaPresenter.isCategoriesDialogShowing = true;
|
UploadMediaPresenter.isCategoriesDialogShowing = true;
|
||||||
// Inflate the custom layout
|
// Inflate the custom layout
|
||||||
LayoutInflater inflater = getLayoutInflater();
|
final LayoutInflater inflater = getLayoutInflater();
|
||||||
View view = inflater.inflate(R.layout.activity_upload_categories_dialog, null);
|
final View view = inflater.inflate(R.layout.activity_upload_categories_dialog, null);
|
||||||
CheckBox checkBox = view.findViewById(R.id.categories_checkbox);
|
final CheckBox checkBox = view.findViewById(R.id.categories_checkbox);
|
||||||
// Create the alert dialog
|
// Create the alert dialog
|
||||||
AlertDialog alertDialog = new AlertDialog.Builder(this)
|
final AlertDialog alertDialog = new AlertDialog.Builder(this)
|
||||||
.setView(view)
|
.setView(view)
|
||||||
.setTitle(getString(R.string.multiple_files_depiction_header))
|
.setTitle(getString(R.string.multiple_files_depiction_header))
|
||||||
.setMessage(getString(R.string.multiple_files_depiction))
|
.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
|
it shows a list of all the apps on the device and allows users to
|
||||||
turn battery optimisation off.
|
turn battery optimisation off.
|
||||||
*/
|
*/
|
||||||
Intent batteryOptimisationSettingsIntent = new Intent(
|
final Intent batteryOptimisationSettingsIntent = new Intent(
|
||||||
Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS);
|
Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS);
|
||||||
startActivity(batteryOptimisationSettingsIntent);
|
startActivity(batteryOptimisationSettingsIntent);
|
||||||
// calling checkImageQuality after battery dialog is interacted with
|
// 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.
|
* conditions are met, returns current location of the user.
|
||||||
*/
|
*/
|
||||||
private void handleLocation(){
|
private void handleLocation(){
|
||||||
LocationPermissionsHelper locationPermissionsHelper = new LocationPermissionsHelper(
|
final LocationPermissionsHelper locationPermissionsHelper = new LocationPermissionsHelper(
|
||||||
this, locationManager, null);
|
this, locationManager, null);
|
||||||
if (locationPermissionsHelper.isLocationAccessToAppsTurnedOn()) {
|
if (locationPermissionsHelper.isLocationAccessToAppsTurnedOn()) {
|
||||||
currLocation = locationManager.getLastLocation();
|
currLocation = locationManager.getLastLocation();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currLocation != null) {
|
if (currLocation != null) {
|
||||||
float locationDifference = getLocationDifference(currLocation, prevLocation);
|
final float locationDifference = getLocationDifference(currLocation, prevLocation);
|
||||||
boolean isLocationTagUnchecked = isLocationTagUncheckedInTheSettings();
|
final boolean isLocationTagUnchecked = isLocationTagUncheckedInTheSettings();
|
||||||
/* Remove location if the user has unchecked the Location EXIF tag in the
|
/* 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.
|
Manage EXIF Tags setting or turned "Record location for in-app shots" off.
|
||||||
Also, location information is discarded if the difference between
|
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.ChunkInfo
|
||||||
import fr.free.nrw.commons.contributions.Contribution
|
import fr.free.nrw.commons.contributions.Contribution
|
||||||
import fr.free.nrw.commons.contributions.ContributionDao
|
import fr.free.nrw.commons.contributions.ContributionDao
|
||||||
|
import fr.free.nrw.commons.di.NetworkingModule
|
||||||
import fr.free.nrw.commons.upload.worker.UploadWorker.NotificationUpdateProgressListener
|
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 fr.free.nrw.commons.wikidata.mwapi.MwException
|
||||||
import io.reactivex.Observable
|
import io.reactivex.Observable
|
||||||
import io.reactivex.disposables.CompositeDisposable
|
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.AtomicInteger
|
||||||
import java.util.concurrent.atomic.AtomicReference
|
import java.util.concurrent.atomic.AtomicReference
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Named
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
|
|
@ -33,7 +36,7 @@ class UploadClient
|
||||||
@Inject
|
@Inject
|
||||||
constructor(
|
constructor(
|
||||||
private val uploadInterface: UploadInterface,
|
private val uploadInterface: UploadInterface,
|
||||||
private val csrfTokenClient: CsrfTokenClient,
|
@Named(NetworkingModule.NAMED_COMMONS_CSRF) private val csrfTokenClient: CsrfTokenClient,
|
||||||
private val pageContentsCreator: PageContentsCreator,
|
private val pageContentsCreator: PageContentsCreator,
|
||||||
private val fileUtilsWrapper: FileUtilsWrapper,
|
private val fileUtilsWrapper: FileUtilsWrapper,
|
||||||
private val gson: Gson,
|
private val gson: Gson,
|
||||||
|
|
@ -66,7 +69,7 @@ class UploadClient
|
||||||
|
|
||||||
val file = contribution.localUriPath
|
val file = contribution.localUriPath
|
||||||
val fileChunks = fileUtilsWrapper.getFileChunks(file, chunkSize)
|
val fileChunks = fileUtilsWrapper.getFileChunks(file, chunkSize)
|
||||||
val mediaType = fileUtilsWrapper.getMimeType(file).toMediaTypeOrNull()
|
val mediaType = fileUtilsWrapper.getMimeType(file)?.toMediaTypeOrNull()
|
||||||
|
|
||||||
val chunkInfo = AtomicReference<ChunkInfo?>()
|
val chunkInfo = AtomicReference<ChunkInfo?>()
|
||||||
if (isStashValid(contribution)) {
|
if (isStashValid(contribution)) {
|
||||||
|
|
@ -278,11 +281,7 @@ class UploadClient
|
||||||
Timber.e(throwable, "Exception occurred in uploading file from stash")
|
Timber.e(throwable, "Exception occurred in uploading file from stash")
|
||||||
Observable.error(throwable)
|
Observable.error(throwable)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
fun interface TimeProvider {
|
|
||||||
fun currentTimeMillis(): Long
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun canProcess(
|
private fun canProcess(
|
||||||
contributionDao: ContributionDao,
|
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() {
|
public Observable<Contribution> buildContributions() {
|
||||||
return Observable.fromIterable(items).map(item ->
|
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(
|
final Contribution contribution = new Contribution(
|
||||||
item, sessionManager, newListOf(selectedDepictions), newListOf(selectedCategories), imageSHA1);
|
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.auth.csrf.InvalidLoginTokenException
|
||||||
import fr.free.nrw.commons.category.CategoryEditHelper
|
import fr.free.nrw.commons.category.CategoryEditHelper
|
||||||
import fr.free.nrw.commons.category.CategoryItem
|
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.IO_THREAD
|
||||||
import fr.free.nrw.commons.di.CommonsApplicationModule.Companion.MAIN_THREAD
|
import fr.free.nrw.commons.di.CommonsApplicationModule.Companion.MAIN_THREAD
|
||||||
import fr.free.nrw.commons.repository.UploadRepository
|
import fr.free.nrw.commons.repository.UploadRepository
|
||||||
|
|
@ -175,7 +174,7 @@ class CategoriesPresenter
|
||||||
) {
|
) {
|
||||||
this.view = view
|
this.view = view
|
||||||
this.media = media
|
this.media = media
|
||||||
repository.setSelectedExistingCategories(view.existingCategories)
|
repository.setSelectedExistingCategories(view.getExistingCategories() ?: emptyList())
|
||||||
compositeDisposable.add(
|
compositeDisposable.add(
|
||||||
searchTerms
|
searchTerms
|
||||||
.observeOn(mainThreadScheduler)
|
.observeOn(mainThreadScheduler)
|
||||||
|
|
@ -224,11 +223,11 @@ class CategoriesPresenter
|
||||||
repository.getSelectedCategories().isNotEmpty()
|
repository.getSelectedCategories().isNotEmpty()
|
||||||
||
|
||
|
||||||
(
|
(
|
||||||
view.existingCategories != null
|
view.getExistingCategories() != null
|
||||||
&&
|
&&
|
||||||
repository.getSelectedExistingCategories().size
|
repository.getSelectedExistingCategories().size
|
||||||
!=
|
!=
|
||||||
view.existingCategories.size
|
view.getExistingCategories()?.size
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
val selectedCategories: MutableList<String> =
|
val selectedCategories: MutableList<String> =
|
||||||
|
|
@ -244,7 +243,7 @@ class CategoriesPresenter
|
||||||
compositeDisposable.add(
|
compositeDisposable.add(
|
||||||
categoryEditHelper
|
categoryEditHelper
|
||||||
.makeCategoryEdit(
|
.makeCategoryEdit(
|
||||||
view.fragmentContext,
|
view.getFragmentContext(),
|
||||||
media,
|
media,
|
||||||
selectedCategories,
|
selectedCategories,
|
||||||
wikiText,
|
wikiText,
|
||||||
|
|
|
||||||
|
|
@ -65,14 +65,14 @@ public class UploadCategoriesFragment extends UploadBaseFragment implements Cate
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
|
public View onCreateView(@NonNull final LayoutInflater inflater, @Nullable final ViewGroup container,
|
||||||
@Nullable Bundle savedInstanceState) {
|
@Nullable final Bundle savedInstanceState) {
|
||||||
binding = UploadCategoriesFragmentBinding.inflate(inflater, container, false);
|
binding = UploadCategoriesFragmentBinding.inflate(inflater, container, false);
|
||||||
return binding.getRoot();
|
return binding.getRoot();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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);
|
super.onViewCreated(view, savedInstanceState);
|
||||||
final Bundle bundle = getArguments();
|
final Bundle bundle = getArguments();
|
||||||
if (bundle != null) {
|
if (bundle != null) {
|
||||||
|
|
@ -104,8 +104,12 @@ public class UploadCategoriesFragment extends UploadBaseFragment implements Cate
|
||||||
setTvSubTitle();
|
setTvSubTitle();
|
||||||
binding.tooltip.setOnClickListener(new OnClickListener() {
|
binding.tooltip.setOnClickListener(new OnClickListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onClick(View v) {
|
public void onClick(final View v) {
|
||||||
DialogUtil.showAlertDialog(getActivity(), getString(R.string.categories_activity_title), getString(R.string.categories_tooltip), getString(android.R.string.ok), null);
|
DialogUtil.showAlertDialog(requireActivity(),
|
||||||
|
getString(R.string.categories_activity_title),
|
||||||
|
getString(R.string.categories_tooltip),
|
||||||
|
getString(android.R.string.ok),
|
||||||
|
null);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (media == 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);
|
presenter.searchForCategories(query);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -170,28 +174,28 @@ public class UploadCategoriesFragment extends UploadBaseFragment implements Cate
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void showProgress(boolean shouldShow) {
|
public void showProgress(final boolean shouldShow) {
|
||||||
if (binding != null) {
|
if (binding != null) {
|
||||||
binding.pbCategories.setVisibility(shouldShow ? View.VISIBLE : View.GONE);
|
binding.pbCategories.setVisibility(shouldShow ? View.VISIBLE : View.GONE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void showError(String error) {
|
public void showError(final String error) {
|
||||||
if (binding != null) {
|
if (binding != null) {
|
||||||
binding.tilContainerSearch.setError(error);
|
binding.tilContainerSearch.setError(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void showError(int stringResourceId) {
|
public void showError(final int stringResourceId) {
|
||||||
if (binding != null) {
|
if (binding != null) {
|
||||||
binding.tilContainerSearch.setError(getString(stringResourceId));
|
binding.tilContainerSearch.setError(getString(stringResourceId));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setCategories(List<CategoryItem> categories) {
|
public void setCategories(final List<CategoryItem> categories) {
|
||||||
if (categories == null) {
|
if (categories == null) {
|
||||||
adapter.clear();
|
adapter.clear();
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -229,12 +233,12 @@ public class UploadCategoriesFragment extends UploadBaseFragment implements Cate
|
||||||
@Override
|
@Override
|
||||||
public void showNoCategorySelected() {
|
public void showNoCategorySelected() {
|
||||||
if (media == null) {
|
if (media == null) {
|
||||||
DialogUtil.showAlertDialog(getActivity(),
|
DialogUtil.showAlertDialog(requireActivity(),
|
||||||
getString(R.string.no_categories_selected),
|
getString(R.string.no_categories_selected),
|
||||||
getString(R.string.no_categories_selected_warning_desc),
|
getString(R.string.no_categories_selected_warning_desc),
|
||||||
getString(R.string.continue_message),
|
getString(R.string.continue_message),
|
||||||
getString(R.string.cancel),
|
getString(R.string.cancel),
|
||||||
() -> goToNextScreen(),
|
this::goToNextScreen,
|
||||||
null);
|
null);
|
||||||
} else {
|
} else {
|
||||||
Toast.makeText(requireContext(), getString(R.string.no_categories_selected),
|
Toast.makeText(requireContext(), getString(R.string.no_categories_selected),
|
||||||
|
|
@ -256,6 +260,7 @@ public class UploadCategoriesFragment extends UploadBaseFragment implements Cate
|
||||||
/**
|
/**
|
||||||
* Returns required context
|
* Returns required context
|
||||||
*/
|
*/
|
||||||
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public Context getFragmentContext() {
|
public Context getFragmentContext() {
|
||||||
return requireContext();
|
return requireContext();
|
||||||
|
|
@ -306,7 +311,7 @@ public class UploadCategoriesFragment extends UploadBaseFragment implements Cate
|
||||||
public void navigateToLoginScreen() {
|
public void navigateToLoginScreen() {
|
||||||
final String username = sessionManager.getUserName();
|
final String username = sessionManager.getUserName();
|
||||||
final CommonsApplication.BaseLogoutListener logoutListener = new CommonsApplication.BaseLogoutListener(
|
final CommonsApplication.BaseLogoutListener logoutListener = new CommonsApplication.BaseLogoutListener(
|
||||||
getActivity(),
|
requireActivity(),
|
||||||
requireActivity().getString(R.string.invalid_login_message),
|
requireActivity().getString(R.string.invalid_login_message),
|
||||||
username
|
username
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,129 +1,125 @@
|
||||||
package fr.free.nrw.commons.upload.depicts;
|
package fr.free.nrw.commons.upload.depicts
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context
|
||||||
import androidx.annotation.NonNull;
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.LiveData;
|
import fr.free.nrw.commons.BasePresenter
|
||||||
import fr.free.nrw.commons.BasePresenter;
|
import fr.free.nrw.commons.Media
|
||||||
import fr.free.nrw.commons.Media;
|
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
|
||||||
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The contract with which DepictsFragment and its presenter would talk to each other
|
* The contract with which DepictsFragment and its presenter would talk to each other
|
||||||
*/
|
*/
|
||||||
public interface DepictsContract {
|
interface DepictsContract {
|
||||||
|
|
||||||
interface View {
|
interface View {
|
||||||
/**
|
/**
|
||||||
* Go to category screen
|
* Go to category screen
|
||||||
*/
|
*/
|
||||||
void goToNextScreen();
|
fun goToNextScreen()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Go to media detail screen
|
* Go to media detail screen
|
||||||
*/
|
*/
|
||||||
void goToPreviousScreen();
|
fun goToPreviousScreen()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* show error in case of no depiction selected
|
* show error in case of no depiction selected
|
||||||
*/
|
*/
|
||||||
void noDepictionSelected();
|
fun noDepictionSelected()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show progress/Hide progress depending on the boolean value
|
* 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
|
* 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
|
* add depictions to list
|
||||||
*/
|
*/
|
||||||
void setDepictsList(List<DepictedItem> depictedItemList);
|
fun setDepictsList(depictedItemList: List<DepictedItem>)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns required context
|
* Returns required context
|
||||||
*/
|
*/
|
||||||
Context getFragmentContext();
|
fun getFragmentContext(): Context
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns to previous fragment
|
* Returns to previous fragment
|
||||||
*/
|
*/
|
||||||
void goBackToPreviousScreen();
|
fun goBackToPreviousScreen()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets existing depictions IDs from media
|
* Gets existing depictions IDs from media
|
||||||
*/
|
*/
|
||||||
List<String> getExistingDepictions();
|
fun getExistingDepictions(): List<String>?
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shows the progress dialog
|
* Shows the progress dialog
|
||||||
*/
|
*/
|
||||||
void showProgressDialog();
|
fun showProgressDialog()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hides the progress dialog
|
* Hides the progress dialog
|
||||||
*/
|
*/
|
||||||
void dismissProgressDialog();
|
fun dismissProgressDialog()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the depictions
|
* Update the depictions
|
||||||
*/
|
*/
|
||||||
void updateDepicts();
|
fun updateDepicts()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Navigate the user to Login Activity
|
* Navigate the user to Login Activity
|
||||||
*/
|
*/
|
||||||
void navigateToLoginScreen();
|
fun navigateToLoginScreen()
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UserActionListener extends BasePresenter<View> {
|
interface UserActionListener : BasePresenter<View> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Takes to previous screen
|
* Takes to previous screen
|
||||||
*/
|
*/
|
||||||
void onPreviousButtonClicked();
|
fun onPreviousButtonClicked()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Listener for the depicted items selected from the list
|
* 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
|
* 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
|
* Selects all associated places (if any) as depictions
|
||||||
*/
|
*/
|
||||||
void selectPlaceDepictions();
|
fun selectPlaceDepictions()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if depictions were selected
|
* Check if depictions were selected
|
||||||
* from the depiction list
|
* from the depiction list
|
||||||
*/
|
*/
|
||||||
void verifyDepictions();
|
fun verifyDepictions()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clears previous selections
|
* Clears previous selections
|
||||||
*/
|
*/
|
||||||
void clearPreviousSelection();
|
fun clearPreviousSelection()
|
||||||
|
|
||||||
LiveData<List<DepictedItem>> getDepictedItems();
|
fun getDepictedItems(): LiveData<List<DepictedItem>>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the depictions
|
* Update the depictions
|
||||||
*/
|
*/
|
||||||
void updateDepictions(Media media);
|
fun updateDepictions(media: Media)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attaches view and 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
|
@Override
|
||||||
public void showError(Boolean value) {
|
public void showError(boolean value) {
|
||||||
if (binding == null) {
|
if (binding == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ import androidx.lifecycle.MutableLiveData
|
||||||
import fr.free.nrw.commons.Media
|
import fr.free.nrw.commons.Media
|
||||||
import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException
|
import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException
|
||||||
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsController
|
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.IO_THREAD
|
||||||
import fr.free.nrw.commons.di.CommonsApplicationModule.Companion.MAIN_THREAD
|
import fr.free.nrw.commons.di.CommonsApplicationModule.Companion.MAIN_THREAD
|
||||||
import fr.free.nrw.commons.repository.UploadRepository
|
import fr.free.nrw.commons.repository.UploadRepository
|
||||||
|
|
@ -208,7 +207,7 @@ class DepictsPresenter
|
||||||
@SuppressLint("CheckResult")
|
@SuppressLint("CheckResult")
|
||||||
override fun updateDepictions(media: Media) {
|
override fun updateDepictions(media: Media) {
|
||||||
if (repository.getSelectedDepictions().isNotEmpty() ||
|
if (repository.getSelectedDepictions().isNotEmpty() ||
|
||||||
repository.getSelectedExistingDepictions().size != view.existingDepictions.size
|
repository.getSelectedExistingDepictions().size != view.getExistingDepictions()?.size
|
||||||
) {
|
) {
|
||||||
view.showProgressDialog()
|
view.showProgressDialog()
|
||||||
val selectedDepictions: MutableList<String> =
|
val selectedDepictions: MutableList<String> =
|
||||||
|
|
@ -225,7 +224,7 @@ class DepictsPresenter
|
||||||
|
|
||||||
compositeDisposable.add(
|
compositeDisposable.add(
|
||||||
depictsHelper
|
depictsHelper
|
||||||
.makeDepictionEdit(view.fragmentContext, media, selectedDepictions)
|
.makeDepictionEdit(view.getFragmentContext(), media, selectedDepictions)
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe({
|
.subscribe({
|
||||||
|
|
@ -256,7 +255,7 @@ class DepictsPresenter
|
||||||
) {
|
) {
|
||||||
this.view = view
|
this.view = view
|
||||||
this.media = media
|
this.media = media
|
||||||
repository.setSelectedExistingDepictions(view.existingDepictions)
|
repository.setSelectedExistingDepictions(view.getExistingDepictions() ?: emptyList())
|
||||||
compositeDisposable.add(
|
compositeDisposable.add(
|
||||||
searchTerm
|
searchTerm
|
||||||
.observeOn(mainThreadScheduler)
|
.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
|
@Override
|
||||||
public void updateLicenseSummary(String licenseSummary, int numberOfItems) {
|
public void updateLicenseSummary(String licenseSummary, Integer numberOfItems) {
|
||||||
String licenseHyperLink = "<a href='" + Utils.licenseUrlFor(licenseSummary) + "'>" +
|
String licenseHyperLink = "<a href='" + Utils.licenseUrlFor(licenseSummary) + "'>" +
|
||||||
getString(Utils.licenseNameFor(licenseSummary)) + "</a><br>";
|
getString(Utils.licenseNameFor(licenseSummary)) + "</a><br>";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package fr.free.nrw.commons.upload.license;
|
package fr.free.nrw.commons.upload.license;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
import fr.free.nrw.commons.Utils;
|
import fr.free.nrw.commons.Utils;
|
||||||
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
||||||
import fr.free.nrw.commons.repository.UploadRepository;
|
import fr.free.nrw.commons.repository.UploadRepository;
|
||||||
|
|
@ -27,14 +28,14 @@ public class MediaLicensePresenter implements MediaLicenseContract.UserActionLis
|
||||||
private MediaLicenseContract.View view = DUMMY;
|
private MediaLicenseContract.View view = DUMMY;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public MediaLicensePresenter(UploadRepository uploadRepository,
|
public MediaLicensePresenter(final UploadRepository uploadRepository,
|
||||||
@Named("default_preferences") JsonKvStore defaultKVStore) {
|
@Named("default_preferences") final JsonKvStore defaultKVStore) {
|
||||||
this.repository = uploadRepository;
|
this.repository = uploadRepository;
|
||||||
this.defaultKVStore = defaultKVStore;
|
this.defaultKVStore = defaultKVStore;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onAttachView(View view) {
|
public void onAttachView(@NonNull final View view) {
|
||||||
this.view = view;
|
this.view = view;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -48,15 +49,15 @@ public class MediaLicensePresenter implements MediaLicenseContract.UserActionLis
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void getLicenses() {
|
public void getLicenses() {
|
||||||
List<String> licenses = repository.getLicenses();
|
final List<String> licenses = repository.getLicenses();
|
||||||
view.setLicenses(licenses);
|
view.setLicenses(licenses);
|
||||||
|
|
||||||
String selectedLicense = defaultKVStore.getString(Prefs.DEFAULT_LICENSE,
|
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
|
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
|
try {//I have to make sure that the stored default license was not one of the deprecated one's
|
||||||
Utils.licenseNameFor(selectedLicense);
|
Utils.licenseNameFor(selectedLicense);
|
||||||
} catch (IllegalStateException exception) {
|
} catch (final IllegalStateException exception) {
|
||||||
Timber.e(exception.getMessage());
|
Timber.e(exception);
|
||||||
selectedLicense = Prefs.Licenses.CC_BY_SA_4;
|
selectedLicense = Prefs.Licenses.CC_BY_SA_4;
|
||||||
defaultKVStore.putString(Prefs.DEFAULT_LICENSE, 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
|
* @param licenseName
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void selectLicense(String licenseName) {
|
public void selectLicense(final String licenseName) {
|
||||||
repository.setSelectedLicense(licenseName);
|
repository.setSelectedLicense(licenseName);
|
||||||
view.updateLicenseSummary(repository.getSelectedLicense(), repository.getCount());
|
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;
|
public static boolean isCategoriesDialogShowing;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public UploadMediaPresenter(UploadRepository uploadRepository,
|
public UploadMediaPresenter(final UploadRepository uploadRepository,
|
||||||
@Named("default_preferences") JsonKvStore defaultKVStore,
|
@Named("default_preferences") final JsonKvStore defaultKVStore,
|
||||||
@Named(IO_THREAD) Scheduler ioScheduler,
|
@Named(IO_THREAD) final Scheduler ioScheduler,
|
||||||
@Named(MAIN_THREAD) Scheduler mainThreadScheduler) {
|
@Named(MAIN_THREAD) final Scheduler mainThreadScheduler) {
|
||||||
this.repository = uploadRepository;
|
this.repository = uploadRepository;
|
||||||
this.defaultKVStore = defaultKVStore;
|
this.defaultKVStore = defaultKVStore;
|
||||||
this.ioScheduler = ioScheduler;
|
this.ioScheduler = ioScheduler;
|
||||||
|
|
@ -91,7 +91,7 @@ public class UploadMediaPresenter implements UserActionListener, SimilarImageInt
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onAttachView(View view) {
|
public void onAttachView(final View view) {
|
||||||
this.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
|
* Sets the Upload Media Details for the corresponding upload item
|
||||||
*
|
|
||||||
* @param uploadMediaDetails
|
|
||||||
* @param uploadItemIndex
|
|
||||||
*/
|
*/
|
||||||
@Override
|
@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);
|
repository.getUploads().get(uploadItemIndex).setMediaDetails(uploadMediaDetails);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Receives the corresponding uploadable file, processes it and return the view with and uplaod item
|
* Receives the corresponding uploadable file, processes it and return the view with and uplaod item
|
||||||
* @param uploadableFile
|
|
||||||
* @param place
|
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void receiveImage(final UploadableFile uploadableFile, final Place place,
|
public void receiveImage(final UploadableFile uploadableFile, final Place place,
|
||||||
LatLng inAppPictureLocation) {
|
final LatLng inAppPictureLocation) {
|
||||||
view.showProgress(true);
|
view.showProgress(true);
|
||||||
compositeDisposable.add(
|
compositeDisposable.add(
|
||||||
repository
|
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.
|
* This method checks for the nearest location that needs images and suggests it to the user.
|
||||||
* @param uploadItem
|
|
||||||
*/
|
*/
|
||||||
private void checkNearbyPlaces(final UploadItem uploadItem) {
|
private void checkNearbyPlaces(final UploadItem uploadItem) {
|
||||||
final Disposable checkNearbyPlaces = Maybe.fromCallable(() -> repository
|
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
|
* @param hasUserRemovedLocation True if user has removed location from the image
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void displayLocDialog(int uploadItemIndex, LatLng inAppPictureLocation,
|
public void displayLocDialog(final int uploadItemIndex, final LatLng inAppPictureLocation,
|
||||||
boolean hasUserRemovedLocation) {
|
final boolean hasUserRemovedLocation) {
|
||||||
final List<UploadItem> uploadItems = repository.getUploads();
|
final List<UploadItem> uploadItems = repository.getUploads();
|
||||||
UploadItem uploadItem = uploadItems.get(uploadItemIndex);
|
final UploadItem uploadItem = uploadItems.get(uploadItemIndex);
|
||||||
if (uploadItem.getGpsCoords().getDecimalCoords() == null && inAppPictureLocation == null
|
if (uploadItem.getGpsCoords().getDecimalCoords() == null && inAppPictureLocation == null
|
||||||
&& !hasUserRemovedLocation) {
|
&& !hasUserRemovedLocation) {
|
||||||
final Runnable onSkipClicked = () -> {
|
final Runnable onSkipClicked = () -> {
|
||||||
|
|
@ -233,7 +227,7 @@ public class UploadMediaPresenter implements UserActionListener, SimilarImageInt
|
||||||
*
|
*
|
||||||
* @param uploadItem UploadItem whose caption is checked
|
* @param uploadItem UploadItem whose caption is checked
|
||||||
*/
|
*/
|
||||||
private void verifyCaptionQuality(UploadItem uploadItem) {
|
private void verifyCaptionQuality(final UploadItem uploadItem) {
|
||||||
view.showProgress(true);
|
view.showProgress(true);
|
||||||
compositeDisposable.add(
|
compositeDisposable.add(
|
||||||
repository
|
repository
|
||||||
|
|
@ -262,7 +256,7 @@ public class UploadMediaPresenter implements UserActionListener, SimilarImageInt
|
||||||
* @param errorCode Error code of the UploadItem
|
* @param errorCode Error code of the UploadItem
|
||||||
* @param uploadItem UploadItem whose caption is checked
|
* @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 is empty caption show message
|
||||||
if (errorCode == EMPTY_CAPTION) {
|
if (errorCode == EMPTY_CAPTION) {
|
||||||
Timber.d("Captions are empty. Showing toast");
|
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
|
* Copies the caption and description of the current item to the subsequent media
|
||||||
*
|
|
||||||
* @param indexInViewFlipper
|
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void copyTitleAndDescriptionToSubsequentMedia(int indexInViewFlipper) {
|
public void copyTitleAndDescriptionToSubsequentMedia(final int indexInViewFlipper) {
|
||||||
for(int i = indexInViewFlipper+1; i < repository.getCount(); i++){
|
for(int i = indexInViewFlipper+1; i < repository.getCount(); i++){
|
||||||
final UploadItem subsequentUploadItem = repository.getUploads().get(i);
|
final UploadItem subsequentUploadItem = repository.getUploads().get(i);
|
||||||
subsequentUploadItem.setMediaDetails(deepCopy(repository.getUploads().get(indexInViewFlipper).getUploadMediaDetails()));
|
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
|
* Fetches and set the caption and description of the item
|
||||||
*
|
|
||||||
* @param indexInViewFlipper
|
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void fetchTitleAndDescription(int indexInViewFlipper) {
|
public void fetchTitleAndDescription(final int indexInViewFlipper) {
|
||||||
final UploadItem currentUploadItem = repository.getUploads().get(indexInViewFlipper);
|
final UploadItem currentUploadItem = repository.getUploads().get(indexInViewFlipper);
|
||||||
view.updateMediaDetails(currentUploadItem.getUploadMediaDetails());
|
view.updateMediaDetails(currentUploadItem.getUploadMediaDetails());
|
||||||
}
|
}
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
private List<UploadMediaDetail> deepCopy(List<UploadMediaDetail> uploadMediaDetails) {
|
private List<UploadMediaDetail> deepCopy(final List<UploadMediaDetail> uploadMediaDetails) {
|
||||||
final ArrayList<UploadMediaDetail> newList = new ArrayList<>();
|
final ArrayList<UploadMediaDetail> newList = new ArrayList<>();
|
||||||
for (UploadMediaDetail uploadMediaDetail : uploadMediaDetails) {
|
for (final UploadMediaDetail uploadMediaDetail : uploadMediaDetails) {
|
||||||
newList.add(uploadMediaDetail.javaCopy());
|
newList.add(uploadMediaDetail.javaCopy());
|
||||||
}
|
}
|
||||||
return newList;
|
return newList;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void useSimilarPictureCoordinates(ImageCoordinates imageCoordinates, int uploadItemIndex) {
|
public void useSimilarPictureCoordinates(final ImageCoordinates imageCoordinates, final int uploadItemIndex) {
|
||||||
repository.useSimilarPictureCoordinates(imageCoordinates, uploadItemIndex);
|
repository.useSimilarPictureCoordinates(imageCoordinates, uploadItemIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onMapIconClicked(int indexInViewFlipper) {
|
public void onMapIconClicked(final int indexInViewFlipper) {
|
||||||
view.showExternalMap(repository.getUploads().get(indexInViewFlipper));
|
view.showExternalMap(repository.getUploads().get(indexInViewFlipper));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onEditButtonClicked(int indexInViewFlipper){
|
public void onEditButtonClicked(final int indexInViewFlipper){
|
||||||
view.showEditActivity(repository.getUploads().get(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.
|
* @param place The place to be associated with the uploads.
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void onUserConfirmedUploadIsOfPlace(Place place) {
|
public void onUserConfirmedUploadIsOfPlace(final Place place) {
|
||||||
final List<UploadItem> uploads = repository.getUploads();
|
final List<UploadItem> uploads = repository.getUploads();
|
||||||
for (UploadItem uploadItem : uploads) {
|
for (final UploadItem uploadItem : uploads) {
|
||||||
uploadItem.setPlace(place);
|
uploadItem.setPlace(place);
|
||||||
final List<UploadMediaDetail> uploadMediaDetails = uploadItem.getUploadMediaDetails();
|
final List<UploadMediaDetail> uploadMediaDetails = uploadItem.getUploadMediaDetails();
|
||||||
// Update UploadMediaDetail object for this UploadItem
|
// 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
|
* @return true if no internal error occurs, else returns false
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public boolean getImageQuality(int uploadItemIndex, LatLng inAppPictureLocation,
|
public boolean getImageQuality(final int uploadItemIndex, final LatLng inAppPictureLocation,
|
||||||
Activity activity) {
|
final Activity activity) {
|
||||||
final List<UploadItem> uploadItems = repository.getUploads();
|
final List<UploadItem> uploadItems = repository.getUploads();
|
||||||
view.showProgress(true);
|
view.showProgress(true);
|
||||||
if (uploadItems.size() == 0) {
|
if (uploadItems.isEmpty()) {
|
||||||
view.showProgress(false);
|
view.showProgress(false);
|
||||||
// No internationalization required for this error message because it's an internal error.
|
// No internationalization required for this error message because it's an internal error.
|
||||||
view.showMessage(
|
view.showMessage(
|
||||||
|
|
@ -374,7 +364,7 @@ public class UploadMediaPresenter implements UserActionListener, SimilarImageInt
|
||||||
R.color.color_error);
|
R.color.color_error);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
UploadItem uploadItem = uploadItems.get(uploadItemIndex);
|
final UploadItem uploadItem = uploadItems.get(uploadItemIndex);
|
||||||
compositeDisposable.add(
|
compositeDisposable.add(
|
||||||
repository
|
repository
|
||||||
.getImageQuality(uploadItem, inAppPictureLocation)
|
.getImageQuality(uploadItem, inAppPictureLocation)
|
||||||
|
|
@ -404,12 +394,12 @@ public class UploadMediaPresenter implements UserActionListener, SimilarImageInt
|
||||||
* @param activity Context reference
|
* @param activity Context reference
|
||||||
* @param uploadItem UploadItem whose quality is to be checked
|
* @param uploadItem UploadItem whose quality is to be checked
|
||||||
*/
|
*/
|
||||||
private void storeImageQuality(Integer imageResult, int uploadItemIndex, Activity activity,
|
private void storeImageQuality(final Integer imageResult, final int uploadItemIndex, final Activity activity,
|
||||||
UploadItem uploadItem) {
|
final UploadItem uploadItem) {
|
||||||
BasicKvStore store = new BasicKvStore(activity,
|
final BasicKvStore store = new BasicKvStore(activity,
|
||||||
UploadActivity.storeNameForCurrentUploadImagesSize);
|
UploadActivity.storeNameForCurrentUploadImagesSize);
|
||||||
String value = store.getString(keyForCurrentUploadImageQualities, null);
|
final String value = store.getString(keyForCurrentUploadImageQualities, null);
|
||||||
JSONObject jsonObject;
|
final JSONObject jsonObject;
|
||||||
try {
|
try {
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
jsonObject = new JSONObject(value);
|
jsonObject = new JSONObject(value);
|
||||||
|
|
@ -418,7 +408,8 @@ public class UploadMediaPresenter implements UserActionListener, SimilarImageInt
|
||||||
}
|
}
|
||||||
jsonObject.put("UploadItem" + uploadItemIndex, imageResult);
|
jsonObject.put("UploadItem" + uploadItemIndex, imageResult);
|
||||||
store.putString(keyForCurrentUploadImageQualities, jsonObject.toString());
|
store.putString(keyForCurrentUploadImageQualities, jsonObject.toString());
|
||||||
} catch (Exception e) {
|
} catch (final Exception e) {
|
||||||
|
Timber.e(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (uploadItemIndex == 0) {
|
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
|
* @param index Index of the UploadItem whose quality is to be checked
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void checkImageQuality(UploadItem uploadItem, int index) {
|
public void checkImageQuality(final UploadItem uploadItem, final int index) {
|
||||||
if ((uploadItem.getImageQuality() != IMAGE_OK) && (uploadItem.getImageQuality()
|
if ((uploadItem.getImageQuality() != IMAGE_OK) && (uploadItem.getImageQuality()
|
||||||
!= IMAGE_KEEP)) {
|
!= IMAGE_KEEP)) {
|
||||||
BasicKvStore store = new BasicKvStore(activity,
|
final BasicKvStore store = new BasicKvStore(activity,
|
||||||
UploadActivity.storeNameForCurrentUploadImagesSize);
|
UploadActivity.storeNameForCurrentUploadImagesSize);
|
||||||
String value = store.getString(keyForCurrentUploadImageQualities, null);
|
final String value = store.getString(keyForCurrentUploadImageQualities, null);
|
||||||
JSONObject jsonObject;
|
final JSONObject jsonObject;
|
||||||
try {
|
try {
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
jsonObject = new JSONObject(value);
|
jsonObject = new JSONObject(value);
|
||||||
} else {
|
} else {
|
||||||
jsonObject = new JSONObject();
|
jsonObject = new JSONObject();
|
||||||
}
|
}
|
||||||
Integer imageQuality = (int) jsonObject.get("UploadItem" + index);
|
final Integer imageQuality = (int) jsonObject.get("UploadItem" + index);
|
||||||
view.showProgress(false);
|
view.showProgress(false);
|
||||||
if (imageQuality == IMAGE_OK) {
|
if (imageQuality == IMAGE_OK) {
|
||||||
uploadItem.setHasInvalidLocation(false);
|
uploadItem.setHasInvalidLocation(false);
|
||||||
|
|
@ -459,7 +450,7 @@ public class UploadMediaPresenter implements UserActionListener, SimilarImageInt
|
||||||
} else {
|
} else {
|
||||||
handleBadImage(imageQuality, uploadItem, index);
|
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
|
* @param index Index of the UploadItem which was deleted
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void updateImageQualitiesJSON(int size, int index) {
|
public void updateImageQualitiesJSON(final int size, final int index) {
|
||||||
BasicKvStore store = new BasicKvStore(activity,
|
final BasicKvStore store = new BasicKvStore(activity,
|
||||||
UploadActivity.storeNameForCurrentUploadImagesSize);
|
UploadActivity.storeNameForCurrentUploadImagesSize);
|
||||||
String value = store.getString(keyForCurrentUploadImageQualities, null);
|
final String value = store.getString(keyForCurrentUploadImageQualities, null);
|
||||||
JSONObject jsonObject;
|
final JSONObject jsonObject;
|
||||||
try {
|
try {
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
jsonObject = new JSONObject(value);
|
jsonObject = new JSONObject(value);
|
||||||
|
|
@ -487,7 +478,8 @@ public class UploadMediaPresenter implements UserActionListener, SimilarImageInt
|
||||||
}
|
}
|
||||||
jsonObject.remove("UploadItem" + (size - 1));
|
jsonObject.remove("UploadItem" + (size - 1));
|
||||||
store.putString(keyForCurrentUploadImageQualities, jsonObject.toString());
|
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 uploadItem UploadItem whose quality is bad
|
||||||
* @param index Index of item whose quality is bad
|
* @param index Index of item whose quality is bad
|
||||||
*/
|
*/
|
||||||
public void handleBadImage(Integer errorCode,
|
public void handleBadImage(final Integer errorCode,
|
||||||
UploadItem uploadItem, int index) {
|
final UploadItem uploadItem, final int index) {
|
||||||
Timber.d("Handle bad picture with error code %d", errorCode);
|
Timber.d("Handle bad picture with error code %d", errorCode);
|
||||||
if (errorCode >= 8) { // If location of image and nearby does not match
|
if (errorCode >= 8) { // If location of image and nearby does not match
|
||||||
uploadItem.setHasInvalidLocation(true);
|
uploadItem.setHasInvalidLocation(true);
|
||||||
|
|
@ -520,9 +512,9 @@ public class UploadMediaPresenter implements UserActionListener, SimilarImageInt
|
||||||
* @param activity Context reference
|
* @param activity Context reference
|
||||||
* @param uploadItem UploadItem which has problems
|
* @param uploadItem UploadItem which has problems
|
||||||
*/
|
*/
|
||||||
public void showBadImagePopup(Integer errorCode,
|
public void showBadImagePopup(final Integer errorCode,
|
||||||
int index, Activity activity, UploadItem uploadItem) {
|
final int index, final Activity activity, final UploadItem uploadItem) {
|
||||||
String errorMessageForResult = getErrorMessageForResult(activity, errorCode);
|
final String errorMessageForResult = getErrorMessageForResult(activity, errorCode);
|
||||||
if (!StringUtils.isBlank(errorMessageForResult)) {
|
if (!StringUtils.isBlank(errorMessageForResult)) {
|
||||||
DialogUtil.showAlertDialog(activity,
|
DialogUtil.showAlertDialog(activity,
|
||||||
activity.getString(R.string.upload_problem_image),
|
activity.getString(R.string.upload_problem_image),
|
||||||
|
|
@ -537,20 +529,16 @@ public class UploadMediaPresenter implements UserActionListener, SimilarImageInt
|
||||||
presenterCallback.deletePictureAtIndex(index);
|
presenterCallback.deletePictureAtIndex(index);
|
||||||
}
|
}
|
||||||
).setCancelable(false);
|
).setCancelable(false);
|
||||||
} else {
|
|
||||||
}
|
}
|
||||||
//If the error message is null, we will probably not show anything
|
//If the error message is null, we will probably not show anything
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* notifies the user that a similar image exists
|
* notifies the user that a similar image exists
|
||||||
* @param originalFilePath
|
|
||||||
* @param possibleFilePath
|
|
||||||
* @param similarImageCoordinates
|
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void showSimilarImageFragment(String originalFilePath, String possibleFilePath,
|
public void showSimilarImageFragment(final String originalFilePath, final String possibleFilePath,
|
||||||
ImageCoordinates similarImageCoordinates) {
|
final ImageCoordinates similarImageCoordinates) {
|
||||||
view.showSimilarImageFragment(originalFilePath, possibleFilePath,
|
view.showSimilarImageFragment(originalFilePath, possibleFilePath,
|
||||||
similarImageCoordinates
|
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 android.content.pm.PackageManager
|
||||||
import fr.free.nrw.commons.BuildConfig
|
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 {
|
object ConfigUtils {
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
val isBetaFlavour: Boolean = BuildConfig.FLAVOR == "beta"
|
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.test.core.app.ApplicationProvider
|
||||||
import androidx.work.Configuration
|
import androidx.work.Configuration
|
||||||
import androidx.work.testing.WorkManagerTestInitHelper
|
import androidx.work.testing.WorkManagerTestInitHelper
|
||||||
import fr.free.nrw.commons.CommonsApplication
|
|
||||||
import fr.free.nrw.commons.OkHttpConnectionFactory
|
import fr.free.nrw.commons.OkHttpConnectionFactory
|
||||||
import fr.free.nrw.commons.R
|
import fr.free.nrw.commons.R
|
||||||
import fr.free.nrw.commons.TestCommonsApplication
|
import fr.free.nrw.commons.TestCommonsApplication
|
||||||
|
|
@ -75,7 +74,7 @@ class UploadActivityUnitTests {
|
||||||
@Test
|
@Test
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
fun testIsLoggedIn() {
|
fun testIsLoggedIn() {
|
||||||
activity.isLoggedIn
|
activity.isLoggedIn()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -139,7 +138,7 @@ class UploadActivityUnitTests {
|
||||||
@Test
|
@Test
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
fun testGetUploadableFiles() {
|
fun testGetUploadableFiles() {
|
||||||
activity.uploadableFiles
|
activity.getUploadableFiles()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
||||||
|
|
@ -8,14 +8,13 @@ import com.nhaarman.mockitokotlin2.anyOrNull
|
||||||
import com.nhaarman.mockitokotlin2.argumentCaptor
|
import com.nhaarman.mockitokotlin2.argumentCaptor
|
||||||
import com.nhaarman.mockitokotlin2.eq
|
import com.nhaarman.mockitokotlin2.eq
|
||||||
import com.nhaarman.mockitokotlin2.mock
|
import com.nhaarman.mockitokotlin2.mock
|
||||||
import com.nhaarman.mockitokotlin2.times
|
|
||||||
import com.nhaarman.mockitokotlin2.whenever
|
import com.nhaarman.mockitokotlin2.whenever
|
||||||
import fr.free.nrw.commons.CommonsApplication.Companion.DEFAULT_EDIT_SUMMARY
|
import fr.free.nrw.commons.CommonsApplication.Companion.DEFAULT_EDIT_SUMMARY
|
||||||
import fr.free.nrw.commons.auth.csrf.CsrfTokenClient
|
import fr.free.nrw.commons.auth.csrf.CsrfTokenClient
|
||||||
import fr.free.nrw.commons.contributions.ChunkInfo
|
import fr.free.nrw.commons.contributions.ChunkInfo
|
||||||
import fr.free.nrw.commons.contributions.Contribution
|
import fr.free.nrw.commons.contributions.Contribution
|
||||||
import fr.free.nrw.commons.contributions.ContributionDao
|
import fr.free.nrw.commons.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.MwException
|
||||||
import fr.free.nrw.commons.wikidata.mwapi.MwServiceError
|
import fr.free.nrw.commons.wikidata.mwapi.MwServiceError
|
||||||
import io.reactivex.Observable
|
import io.reactivex.Observable
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,7 @@ class UploadPresenterTest {
|
||||||
uploadPresenter.onAttachView(view)
|
uploadPresenter.onAttachView(view)
|
||||||
`when`(repository.buildContributions()).thenReturn(Observable.just(contribution))
|
`when`(repository.buildContributions()).thenReturn(Observable.just(contribution))
|
||||||
uploadableFiles.add(uploadableFile)
|
uploadableFiles.add(uploadableFile)
|
||||||
`when`(view.uploadableFiles).thenReturn(uploadableFiles)
|
`when`(view.getUploadableFiles()).thenReturn(uploadableFiles)
|
||||||
`when`(uploadableFile.getFilePath()).thenReturn("data://test")
|
`when`(uploadableFile.getFilePath()).thenReturn("data://test")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -71,9 +71,9 @@ class UploadPresenterTest {
|
||||||
@Ignore
|
@Ignore
|
||||||
@Test
|
@Test
|
||||||
fun handleSubmitTestUserLoggedIn() {
|
fun handleSubmitTestUserLoggedIn() {
|
||||||
`when`(view.isLoggedIn).thenReturn(true)
|
`when`(view.isLoggedIn()).thenReturn(true)
|
||||||
uploadPresenter.handleSubmit()
|
uploadPresenter.handleSubmit()
|
||||||
verify(view).isLoggedIn
|
verify(view).isLoggedIn()
|
||||||
verify(view).showProgress(true)
|
verify(view).showProgress(true)
|
||||||
verify(repository).buildContributions()
|
verify(repository).buildContributions()
|
||||||
verify(repository).buildContributions()
|
verify(repository).buildContributions()
|
||||||
|
|
@ -130,9 +130,9 @@ class UploadPresenterTest {
|
||||||
false,
|
false,
|
||||||
),
|
),
|
||||||
).thenReturn(true)
|
).thenReturn(true)
|
||||||
`when`(view.isLoggedIn).thenReturn(true)
|
`when`(view.isLoggedIn()).thenReturn(true)
|
||||||
uploadPresenter.handleSubmit()
|
uploadPresenter.handleSubmit()
|
||||||
verify(view).isLoggedIn
|
verify(view).isLoggedIn()
|
||||||
verify(view).showProgress(true)
|
verify(view).showProgress(true)
|
||||||
verify(repository).buildContributions()
|
verify(repository).buildContributions()
|
||||||
verify(repository).buildContributions()
|
verify(repository).buildContributions()
|
||||||
|
|
@ -144,9 +144,9 @@ class UploadPresenterTest {
|
||||||
@Ignore
|
@Ignore
|
||||||
@Test
|
@Test
|
||||||
fun handleSubmitTestUserNotLoggedIn() {
|
fun handleSubmitTestUserNotLoggedIn() {
|
||||||
`when`(view.isLoggedIn).thenReturn(false)
|
`when`(view.isLoggedIn()).thenReturn(false)
|
||||||
uploadPresenter.handleSubmit()
|
uploadPresenter.handleSubmit()
|
||||||
verify(view).isLoggedIn
|
verify(view).isLoggedIn()
|
||||||
verify(view).askUserToLogIn()
|
verify(view).askUserToLogIn()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ import android.view.LayoutInflater
|
||||||
import androidx.fragment.app.FragmentManager
|
import androidx.fragment.app.FragmentManager
|
||||||
import androidx.fragment.app.FragmentTransaction
|
import androidx.fragment.app.FragmentTransaction
|
||||||
import androidx.test.core.app.ApplicationProvider
|
import androidx.test.core.app.ApplicationProvider
|
||||||
import com.nhaarman.mockitokotlin2.times
|
|
||||||
import fr.free.nrw.commons.Media
|
import fr.free.nrw.commons.Media
|
||||||
import fr.free.nrw.commons.OkHttpConnectionFactory
|
import fr.free.nrw.commons.OkHttpConnectionFactory
|
||||||
import fr.free.nrw.commons.R
|
import fr.free.nrw.commons.R
|
||||||
|
|
@ -184,14 +183,14 @@ class UploadCategoriesFragmentUnitTests {
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
fun testGetExistingCategories() {
|
fun testGetExistingCategories() {
|
||||||
Shadows.shadowOf(Looper.getMainLooper()).idle()
|
Shadows.shadowOf(Looper.getMainLooper()).idle()
|
||||||
fragment.existingCategories
|
fragment.getExistingCategories()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
fun testGetFragmentContext() {
|
fun testGetFragmentContext() {
|
||||||
Shadows.shadowOf(Looper.getMainLooper()).idle()
|
Shadows.shadowOf(Looper.getMainLooper()).idle()
|
||||||
fragment.fragmentContext
|
fragment.getFragmentContext()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
||||||
|
|
@ -258,7 +258,7 @@ class DepictsFragmentUnitTests {
|
||||||
@Test
|
@Test
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
fun testGetFragmentContext() {
|
fun testGetFragmentContext() {
|
||||||
fragment.fragmentContext
|
fragment.getFragmentContext()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue