Convert UploadMediaPresenter to kotlin

This commit is contained in:
Paul Hawke 2024-12-31 07:37:18 -06:00
parent e4b4ceb39d
commit 69f804438e
9 changed files with 530 additions and 613 deletions

View file

@ -46,7 +46,7 @@ class UploadRepository @Inject constructor(
* *
* @return * @return
*/ */
fun buildContributions(): Observable<Contribution>? { fun buildContributions(): Observable<Contribution> {
return uploadModel.buildContributions() return uploadModel.buildContributions()
} }
@ -177,7 +177,7 @@ class UploadRepository @Inject constructor(
place: Place?, place: Place?,
similarImageInterface: SimilarImageInterface?, similarImageInterface: SimilarImageInterface?,
inAppPictureLocation: LatLng? inAppPictureLocation: LatLng?
): Observable<UploadItem>? { ): Observable<UploadItem> {
return uploadModel.preProcessImage( return uploadModel.preProcessImage(
uploadableFile, uploadableFile,
place, place,
@ -193,7 +193,7 @@ class UploadRepository @Inject constructor(
* @param location Location of the image * @param location Location of the image
* @return Quality of UploadItem * @return Quality of UploadItem
*/ */
fun getImageQuality(uploadItem: UploadItem, location: LatLng?): Single<Int>? { fun getImageQuality(uploadItem: UploadItem, location: LatLng?): Single<Int> {
return uploadModel.getImageQuality(uploadItem, location) return uploadModel.getImageQuality(uploadItem, location)
} }
@ -213,7 +213,7 @@ class UploadRepository @Inject constructor(
* @param uploadItem UploadItem whose caption is to be checked * @param uploadItem UploadItem whose caption is to be checked
* @return Quality of caption of the UploadItem * @return Quality of caption of the UploadItem
*/ */
fun getCaptionQuality(uploadItem: UploadItem): Single<Int>? { fun getCaptionQuality(uploadItem: UploadItem): Single<Int> {
return uploadModel.getCaptionQuality(uploadItem) return uploadModel.getCaptionQuality(uploadItem)
} }

View file

@ -175,11 +175,11 @@ class UploadModel @Inject internal constructor(
Timber.d( Timber.d(
"Created timestamp while building contribution is %s, %s", "Created timestamp while building contribution is %s, %s",
item.createdTimestamp, item.createdTimestamp,
Date(item.createdTimestamp!!) item.createdTimestamp?.let { Date(it) }
) )
if (item.createdTimestamp != -1L) { if (item.createdTimestamp != -1L) {
contribution.dateCreated = Date(item.createdTimestamp) contribution.dateCreated = item.createdTimestamp?.let { Date(it) }
contribution.dateCreatedSource = item.createdTimestampSource contribution.dateCreatedSource = item.createdTimestampSource
//Set the date only if you have it, else the upload service is gonna try it the other way //Set the date only if you have it, else the upload service is gonna try it the other way
} }

View file

@ -1,7 +1,6 @@
package fr.free.nrw.commons.upload package fr.free.nrw.commons.upload
import android.annotation.SuppressLint import android.annotation.SuppressLint
import fr.free.nrw.commons.CommonsApplication
import fr.free.nrw.commons.CommonsApplication.Companion.IS_LIMITED_CONNECTION_MODE_ENABLED import fr.free.nrw.commons.CommonsApplication.Companion.IS_LIMITED_CONNECTION_MODE_ENABLED
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
@ -69,8 +68,7 @@ class UploadPresenter @Inject internal constructor(
private fun processContributionsForSubmission() { private fun processContributionsForSubmission() {
if (view.isLoggedIn()) { if (view.isLoggedIn()) {
view.showProgress(true) view.showProgress(true)
repository.buildContributions() repository.buildContributions().observeOn(Schedulers.io())
?.observeOn(Schedulers.io())
?.subscribe(object : Observer<Contribution> { ?.subscribe(object : Observer<Contribution> {
override fun onSubscribe(d: Disposable) { override fun onSubscribe(d: Disposable) {
view.showProgress(false) view.showProgress(false)
@ -133,8 +131,9 @@ class UploadPresenter @Inject internal constructor(
* @param uploadItemIndex Index of next image, whose quality is to be checked * @param uploadItemIndex Index of next image, whose quality is to be checked
*/ */
override fun checkImageQuality(uploadItemIndex: Int) { override fun checkImageQuality(uploadItemIndex: Int) {
val uploadItem = repository.getUploadItem(uploadItemIndex) repository.getUploadItem(uploadItemIndex)?.let {
presenter.checkImageQuality(uploadItem, uploadItemIndex) presenter.checkImageQuality(it, uploadItemIndex)
}
} }
override fun deletePictureAtIndex(index: Int) { override fun deletePictureAtIndex(index: Int) {
@ -156,8 +155,9 @@ class UploadPresenter @Inject internal constructor(
view.onUploadMediaDeleted(index) view.onUploadMediaDeleted(index)
if (index != uploadableFiles.size && index != 0) { if (index != uploadableFiles.size && index != 0) {
// if the deleted image was not the last item to be uploaded, check quality of next // if the deleted image was not the last item to be uploaded, check quality of next
val uploadItem = repository.getUploadItem(index) repository.getUploadItem(index)?.let {
presenter.checkImageQuality(uploadItem, index) presenter.checkImageQuality(it, index)
}
} }
if (uploadableFiles.size < 2) { if (uploadableFiles.size < 2) {

View file

@ -56,6 +56,7 @@ import java.util.Locale;
import java.util.Objects; import java.util.Objects;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Named; import javax.inject.Named;
import org.jetbrains.annotations.NotNull;
import timber.log.Timber; import timber.log.Timber;
public class UploadMediaDetailFragment extends UploadBaseFragment implements public class UploadMediaDetailFragment extends UploadBaseFragment implements
@ -154,7 +155,7 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
public void setCallback(UploadMediaDetailFragmentCallback callback) { public void setCallback(UploadMediaDetailFragmentCallback callback) {
this.callback = callback; this.callback = callback;
UploadMediaPresenter.presenterCallback = callback; UploadMediaPresenter.Companion.setPresenterCallback(callback);
} }
@Override @Override
@ -190,12 +191,12 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState); super.onViewCreated(view, savedInstanceState);
activity = getActivity(); activity = requireActivity();
basicKvStore = new BasicKvStore(activity, "CurrentUploadImageQualities"); basicKvStore = new BasicKvStore(activity, "CurrentUploadImageQualities");
if (callback != null) { if (callback != null) {
indexOfFragment = callback.getIndexInViewFlipper(this); indexOfFragment = callback.getIndexInViewFlipper(this);
init(); initializeFragment();
} }
if(savedInstanceState!=null){ if(savedInstanceState!=null){
@ -207,7 +208,7 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
} }
try { try {
if(!presenter.getImageQuality(indexOfFragment, inAppPictureLocation, getActivity())) { if(!presenter.getImageQuality(indexOfFragment, inAppPictureLocation, requireActivity())) {
ActivityUtils.startActivityWithFlags( ActivityUtils.startActivityWithFlags(
getActivity(), MainActivity.class, Intent.FLAG_ACTIVITY_CLEAR_TOP, getActivity(), MainActivity.class, Intent.FLAG_ACTIVITY_CLEAR_TOP,
Intent.FLAG_ACTIVITY_SINGLE_TOP); Intent.FLAG_ACTIVITY_SINGLE_TOP);
@ -217,7 +218,7 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
} }
private void init() { private void initializeFragment() {
if (binding == null) { if (binding == null) {
return; return;
} }
@ -373,7 +374,7 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
} }
@Override @Override
public void onImageProcessed(UploadItem uploadItem, Place place) { public void onImageProcessed(@NotNull UploadItem uploadItem) {
if (binding == null) { if (binding == null) {
return; return;
} }
@ -386,7 +387,8 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
* @param place * @param place
*/ */
@Override @Override
public void onNearbyPlaceFound(UploadItem uploadItem, Place place) { public void onNearbyPlaceFound(
@NotNull UploadItem uploadItem, @org.jetbrains.annotations.Nullable Place place) {
nearbyPlace = place; nearbyPlace = place;
this.uploadItem = uploadItem; this.uploadItem = uploadItem;
showNearbyFound = true; showNearbyFound = true;
@ -506,7 +508,7 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
} }
@Override @Override
public void showDuplicatePicturePopup(UploadItem uploadItem) { public void showDuplicatePicturePopup(@NotNull UploadItem uploadItem) {
if (defaultKvStore.getBoolean("showDuplicatePicturePopup", true)) { if (defaultKvStore.getBoolean("showDuplicatePicturePopup", true)) {
String uploadTitleFormat = getString(R.string.upload_title_duplicate); String uploadTitleFormat = getString(R.string.upload_title_duplicate);
View checkBoxView = View View checkBoxView = View
@ -517,7 +519,7 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
defaultKvStore.putBoolean("showDuplicatePicturePopup", false); defaultKvStore.putBoolean("showDuplicatePicturePopup", false);
} }
}); });
DialogUtil.showAlertDialog(getActivity(), DialogUtil.showAlertDialog(requireActivity(),
getString(R.string.duplicate_file_name), getString(R.string.duplicate_file_name),
String.format(Locale.getDefault(), String.format(Locale.getDefault(),
uploadTitleFormat, uploadTitleFormat,
@ -597,7 +599,7 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
} }
@Override @Override
public void showExternalMap(final UploadItem uploadItem) { public void showExternalMap(@NotNull final UploadItem uploadItem) {
goToLocationPickerActivity(uploadItem); goToLocationPickerActivity(uploadItem);
} }
@ -612,7 +614,7 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
* is started using resultLauncher that handles the result in respective callback. * is started using resultLauncher that handles the result in respective callback.
*/ */
@Override @Override
public void showEditActivity(UploadItem uploadItem) { public void showEditActivity(@NotNull UploadItem uploadItem) {
editableUploadItem = uploadItem; editableUploadItem = uploadItem;
Intent intent = new Intent(getContext(), EditActivity.class); Intent intent = new Intent(getContext(), EditActivity.class);
intent.putExtra("image", uploadableFile.getFilePath().toString()); intent.putExtra("image", uploadableFile.getFilePath().toString());
@ -789,7 +791,7 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
} }
@Override @Override
public void updateMediaDetails(List<UploadMediaDetail> uploadMediaDetails) { public void updateMediaDetails(@NotNull List<UploadMediaDetail> uploadMediaDetails) {
uploadMediaDetailAdapter.setItems(uploadMediaDetails); uploadMediaDetailAdapter.setItems(uploadMediaDetails);
showNearbyFound = showNearbyFound =
showNearbyFound && ( showNearbyFound && (
@ -823,7 +825,7 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
* @param onSkipClicked proceed for verifying image quality * @param onSkipClicked proceed for verifying image quality
*/ */
@Override @Override
public void displayAddLocationDialog(final Runnable onSkipClicked) { public void displayAddLocationDialog(@NotNull final Runnable onSkipClicked) {
isMissingLocationDialog = true; isMissingLocationDialog = true;
DialogUtil.showAlertDialog(requireActivity(), DialogUtil.showAlertDialog(requireActivity(),
getString(R.string.no_location_found_title), getString(R.string.no_location_found_title),

View file

@ -15,9 +15,9 @@ import fr.free.nrw.commons.upload.UploadMediaDetail
*/ */
interface UploadMediaDetailsContract { interface UploadMediaDetailsContract {
interface View : SimilarImageInterface { interface View : SimilarImageInterface {
fun onImageProcessed(uploadItem: UploadItem?, place: Place?) fun onImageProcessed(uploadItem: UploadItem)
fun onNearbyPlaceFound(uploadItem: UploadItem?, place: Place?) fun onNearbyPlaceFound(uploadItem: UploadItem, place: Place?)
fun showProgress(shouldShow: Boolean) fun showProgress(shouldShow: Boolean)
@ -27,7 +27,7 @@ interface UploadMediaDetailsContract {
fun showMessage(message: String?, colorResourceId: Int) fun showMessage(message: String?, colorResourceId: Int)
fun showDuplicatePicturePopup(uploadItem: UploadItem?) fun showDuplicatePicturePopup(uploadItem: UploadItem)
/** /**
* Shows a dialog alerting the user that internet connection is required for upload process * Shows a dialog alerting the user that internet connection is required for upload process
@ -42,13 +42,13 @@ interface UploadMediaDetailsContract {
*/ */
fun showConnectionErrorPopupForCaptionCheck() fun showConnectionErrorPopupForCaptionCheck()
fun showExternalMap(uploadItem: UploadItem?) fun showExternalMap(uploadItem: UploadItem)
fun showEditActivity(uploadItem: UploadItem?) fun showEditActivity(uploadItem: UploadItem)
fun updateMediaDetails(uploadMediaDetails: List<UploadMediaDetail?>?) fun updateMediaDetails(uploadMediaDetails: List<UploadMediaDetail>)
fun displayAddLocationDialog(runnable: Runnable?) fun displayAddLocationDialog(runnable: Runnable)
} }
interface UserActionListener : BasePresenter<View?> { interface UserActionListener : BasePresenter<View?> {
@ -59,7 +59,7 @@ interface UploadMediaDetailsContract {
) )
fun setUploadMediaDetails( fun setUploadMediaDetails(
uploadMediaDetails: List<UploadMediaDetail?>?, uploadMediaDetails: List<UploadMediaDetail>,
uploadItemIndex: Int uploadItemIndex: Int
) )
@ -74,7 +74,7 @@ interface UploadMediaDetailsContract {
fun getImageQuality( fun getImageQuality(
uploadItemIndex: Int, uploadItemIndex: Int,
inAppPictureLocation: LatLng?, inAppPictureLocation: LatLng?,
activity: Activity? activity: Activity
): Boolean ): Boolean
/** /**
@ -87,7 +87,8 @@ interface UploadMediaDetailsContract {
* @param hasUserRemovedLocation True if user has removed location from the image * @param hasUserRemovedLocation True if user has removed location from the image
*/ */
fun displayLocDialog( fun displayLocDialog(
uploadItemIndex: Int, inAppPictureLocation: LatLng?, uploadItemIndex: Int,
inAppPictureLocation: LatLng?,
hasUserRemovedLocation: Boolean hasUserRemovedLocation: Boolean
) )
@ -97,7 +98,7 @@ interface UploadMediaDetailsContract {
* @param uploadItem UploadItem whose quality is to be checked * @param uploadItem UploadItem whose quality is to be checked
* @param index Index of the 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) fun checkImageQuality(uploadItem: UploadItem, index: Int)
/** /**
* Updates the image qualities stored in JSON, whenever an image is deleted * Updates the image qualities stored in JSON, whenever an image is deleted
@ -111,7 +112,7 @@ interface UploadMediaDetailsContract {
fun fetchTitleAndDescription(indexInViewFlipper: Int) fun fetchTitleAndDescription(indexInViewFlipper: Int)
fun useSimilarPictureCoordinates(imageCoordinates: ImageCoordinates?, uploadItemIndex: Int) fun useSimilarPictureCoordinates(imageCoordinates: ImageCoordinates, uploadItemIndex: Int)
fun onMapIconClicked(indexInViewFlipper: Int) fun onMapIconClicked(indexInViewFlipper: Int)

View file

@ -1,547 +0,0 @@
package fr.free.nrw.commons.upload.mediaDetails;
import static fr.free.nrw.commons.di.CommonsApplicationModule.IO_THREAD;
import static fr.free.nrw.commons.di.CommonsApplicationModule.MAIN_THREAD;
import static fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment.activity;
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_KEEP;
import static fr.free.nrw.commons.utils.ImageUtils.IMAGE_OK;
import static fr.free.nrw.commons.utils.ImageUtils.getErrorMessageForResult;
import android.app.Activity;
import androidx.annotation.Nullable;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.filepicker.UploadableFile;
import fr.free.nrw.commons.kvstore.BasicKvStore;
import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.nearby.Place;
import fr.free.nrw.commons.repository.UploadRepository;
import fr.free.nrw.commons.upload.ImageCoordinates;
import fr.free.nrw.commons.upload.SimilarImageInterface;
import fr.free.nrw.commons.upload.UploadActivity;
import fr.free.nrw.commons.upload.UploadItem;
import fr.free.nrw.commons.upload.UploadMediaDetail;
import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment.UploadMediaDetailFragmentCallback;
import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailsContract.UserActionListener;
import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailsContract.View;
import fr.free.nrw.commons.utils.DialogUtil;
import io.github.coordinates2country.Coordinates2Country;
import io.reactivex.Maybe;
import io.reactivex.Scheduler;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.disposables.Disposable;
import java.lang.reflect.Proxy;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import javax.inject.Inject;
import javax.inject.Named;
import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.json.JSONObject;
import timber.log.Timber;
public class UploadMediaPresenter implements UserActionListener, SimilarImageInterface {
private static final UploadMediaDetailsContract.View DUMMY = (UploadMediaDetailsContract.View) Proxy
.newProxyInstance(
UploadMediaDetailsContract.View.class.getClassLoader(),
new Class[]{UploadMediaDetailsContract.View.class},
(proxy, method, methodArgs) -> null);
private final UploadRepository repository;
private UploadMediaDetailsContract.View view = DUMMY;
private CompositeDisposable compositeDisposable;
private final JsonKvStore defaultKVStore;
private Scheduler ioScheduler;
private Scheduler mainThreadScheduler;
public static UploadMediaDetailFragmentCallback presenterCallback ;
private final List<String> WLM_SUPPORTED_COUNTRIES= Arrays.asList("am","at","az","br","hr","sv","fi","fr","de","gh","in","ie","il","mk","my","mt","pk","pe","pl","ru","rw","si","es","se","tw","ug","ua","us");
private Map<String, String> countryNamesAndCodes = null;
private final String keyForCurrentUploadImageQualities = "UploadedImagesQualities";
/**
* Variable used to determine if the battery-optimisation dialog is being shown or not
*/
public static boolean isBatteryDialogShowing;
public static boolean isCategoriesDialogShowing;
@Inject
public UploadMediaPresenter(final UploadRepository uploadRepository,
@Named("default_preferences") final JsonKvStore defaultKVStore,
@Named(IO_THREAD) final Scheduler ioScheduler,
@Named(MAIN_THREAD) final Scheduler mainThreadScheduler) {
this.repository = uploadRepository;
this.defaultKVStore = defaultKVStore;
this.ioScheduler = ioScheduler;
this.mainThreadScheduler = mainThreadScheduler;
compositeDisposable = new CompositeDisposable();
}
@Override
public void onAttachView(final View view) {
this.view = view;
}
@Override
public void onDetachView() {
this.view = DUMMY;
compositeDisposable.clear();
}
/**
* Sets the Upload Media Details for the corresponding upload item
*/
@Override
public void setUploadMediaDetails(final List<UploadMediaDetail> uploadMediaDetails, final int uploadItemIndex) {
repository.getUploads().get(uploadItemIndex).setUploadMediaDetails(uploadMediaDetails);
}
/**
* Receives the corresponding uploadable file, processes it and return the view with and uplaod item
*/
@Override
public void receiveImage(final UploadableFile uploadableFile, final Place place,
final LatLng inAppPictureLocation) {
view.showProgress(true);
compositeDisposable.add(
repository
.preProcessImage(uploadableFile, place, this, inAppPictureLocation)
.map(uploadItem -> {
if(place!=null && place.isMonument()){
if (place.location != null) {
final String countryCode = reverseGeoCode(place.location);
if (countryCode != null && WLM_SUPPORTED_COUNTRIES
.contains(countryCode.toLowerCase(Locale.ROOT))) {
uploadItem.setWLMUpload(true);
uploadItem.setCountryCode(countryCode.toLowerCase(Locale.ROOT));
}
}
}
return uploadItem;
})
.subscribeOn(ioScheduler)
.observeOn(mainThreadScheduler)
.subscribe(uploadItem ->
{
view.onImageProcessed(uploadItem, place);
view.updateMediaDetails(uploadItem.getUploadMediaDetails());
view.showProgress(false);
final ImageCoordinates gpsCoords = uploadItem.getGpsCoords();
final boolean hasImageCoordinates =
gpsCoords != null && gpsCoords.getImageCoordsExists();
if (hasImageCoordinates && place == null) {
checkNearbyPlaces(uploadItem);
}
},
throwable -> Timber.e(throwable, "Error occurred in processing images")));
}
@Nullable
private String reverseGeoCode(final LatLng latLng){
if(countryNamesAndCodes == null){
countryNamesAndCodes = getCountryNamesAndCodes();
}
return countryNamesAndCodes.get(Coordinates2Country.country(latLng.getLatitude(), latLng.getLongitude()));
}
/**
* Creates HashMap containing all ISO countries 2-letter codes provided by <code>Locale.getISOCountries()</code>
* and their english names
*
* @return HashMap where Key is country english name and Value is 2-letter country code
* e.g. ["Germany":"DE", ...]
*/
private Map<String, String> getCountryNamesAndCodes(){
final Map<String, String> result = new HashMap<>();
final String[] isoCountries = Locale.getISOCountries();
for (final String isoCountry : isoCountries) {
result.put(
new Locale("en", isoCountry).getDisplayCountry(Locale.ENGLISH),
isoCountry
);
}
return result;
}
/**
* This method checks for the nearest location that needs images and suggests it to the user.
*/
private void checkNearbyPlaces(final UploadItem uploadItem) {
final Disposable checkNearbyPlaces = Maybe.fromCallable(() -> repository
.checkNearbyPlaces(uploadItem.getGpsCoords().getDecLatitude(),
uploadItem.getGpsCoords().getDecLongitude()))
.subscribeOn(ioScheduler)
.observeOn(mainThreadScheduler)
.subscribe(place -> {
if (place != null) {
view.onNearbyPlaceFound(uploadItem, place);
}
},
throwable -> Timber.e(throwable, "Error occurred in processing images"));
compositeDisposable.add(checkNearbyPlaces);
}
/**
* 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
*/
@Override
public void displayLocDialog(final int uploadItemIndex, final LatLng inAppPictureLocation,
final boolean hasUserRemovedLocation) {
final List<UploadItem> uploadItems = repository.getUploads();
final UploadItem uploadItem = uploadItems.get(uploadItemIndex);
if (uploadItem.getGpsCoords().getDecimalCoords() == null && inAppPictureLocation == null
&& !hasUserRemovedLocation) {
final Runnable onSkipClicked = () -> {
verifyCaptionQuality(uploadItem);
};
view.displayAddLocationDialog(onSkipClicked);
} else {
verifyCaptionQuality(uploadItem);
}
}
/**
* Verifies the image's caption and calls function to handle the result
*
* @param uploadItem UploadItem whose caption is checked
*/
private void verifyCaptionQuality(final UploadItem uploadItem) {
view.showProgress(true);
compositeDisposable.add(
repository
.getCaptionQuality(uploadItem)
.observeOn(mainThreadScheduler)
.subscribe(capResult -> {
view.showProgress(false);
handleCaptionResult(capResult, uploadItem);
},
throwable -> {
view.showProgress(false);
if (throwable instanceof UnknownHostException) {
view.showConnectionErrorPopupForCaptionCheck();
} else {
view.showMessage("" + throwable.getLocalizedMessage(),
R.color.color_error);
}
Timber.e(throwable, "Error occurred while handling image");
})
);
}
/**
* Handles image's caption results and shows dialog if necessary
*
* @param errorCode Error code of the UploadItem
* @param uploadItem UploadItem whose caption is checked
*/
public void handleCaptionResult(final Integer errorCode, final UploadItem uploadItem) {
// If errorCode is empty caption show message
if (errorCode == EMPTY_CAPTION) {
Timber.d("Captions are empty. Showing toast");
view.showMessage(R.string.add_caption_toast, R.color.color_error);
}
// If image with same file name exists check the bit in errorCode is set or not
if ((errorCode & FILE_NAME_EXISTS) != 0) {
Timber.d("Trying to show duplicate picture popup");
view.showDuplicatePicturePopup(uploadItem);
}
// If caption is not duplicate or user still wants to upload it
if (errorCode == IMAGE_OK) {
Timber.d("Image captions are okay or user still wants to upload it");
view.onImageValidationSuccess();
}
}
/**
* Copies the caption and description of the current item to the subsequent media
*/
@Override
public void copyTitleAndDescriptionToSubsequentMedia(final int indexInViewFlipper) {
for(int i = indexInViewFlipper+1; i < repository.getCount(); i++){
final UploadItem subsequentUploadItem = repository.getUploads().get(i);
subsequentUploadItem.setUploadMediaDetails(deepCopy(repository.getUploads().get(indexInViewFlipper).getUploadMediaDetails()));
}
}
/**
* Fetches and set the caption and description of the item
*/
@Override
public void fetchTitleAndDescription(final int indexInViewFlipper) {
final UploadItem currentUploadItem = repository.getUploads().get(indexInViewFlipper);
view.updateMediaDetails(currentUploadItem.getUploadMediaDetails());
}
@NotNull
private List<UploadMediaDetail> deepCopy(final List<UploadMediaDetail> uploadMediaDetails) {
final ArrayList<UploadMediaDetail> newList = new ArrayList<>();
for (final UploadMediaDetail uploadMediaDetail : uploadMediaDetails) {
newList.add(uploadMediaDetail.javaCopy());
}
return newList;
}
@Override
public void useSimilarPictureCoordinates(final ImageCoordinates imageCoordinates, final int uploadItemIndex) {
repository.useSimilarPictureCoordinates(imageCoordinates, uploadItemIndex);
}
@Override
public void onMapIconClicked(final int indexInViewFlipper) {
view.showExternalMap(repository.getUploads().get(indexInViewFlipper));
}
@Override
public void onEditButtonClicked(final int indexInViewFlipper){
view.showEditActivity(repository.getUploads().get(indexInViewFlipper));
}
/**
* Updates the information regarding the specified place for the specified upload item
* when the user confirms the suggested nearby place.
*
* @param place The place to be associated with the uploads.
* @param uploadItemIndex Index of the uploadItem whose detected place has been confirmed
*/
@Override
public void onUserConfirmedUploadIsOfPlace(final Place place, final int uploadItemIndex) {
final UploadItem uploadItem = repository.getUploads().get(uploadItemIndex);
uploadItem.setPlace(place);
final List<UploadMediaDetail> uploadMediaDetails = uploadItem.getUploadMediaDetails();
// Update UploadMediaDetail object for this UploadItem
uploadMediaDetails.set(0, new UploadMediaDetail(place));
// Now that the UploadItem and its associated UploadMediaDetail objects have been updated,
// update the view with the modified media details of the first upload item
view.updateMediaDetails(uploadMediaDetails);
UploadActivity.setUploadIsOfAPlace(true);
}
/**
* 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
*/
@Override
public boolean getImageQuality(final int uploadItemIndex, final LatLng inAppPictureLocation,
final Activity activity) {
final List<UploadItem> uploadItems = repository.getUploads();
view.showProgress(true);
if (uploadItems.isEmpty()) {
view.showProgress(false);
// No internationalization required for this error message because it's an internal error.
view.showMessage(
"Internal error: Zero upload items received by the Upload Media Detail Fragment. Sorry, please upload again.",
R.color.color_error);
return false;
}
final UploadItem uploadItem = uploadItems.get(uploadItemIndex);
compositeDisposable.add(
repository
.getImageQuality(uploadItem, inAppPictureLocation)
.observeOn(mainThreadScheduler)
.subscribe(imageResult -> {
storeImageQuality(imageResult, uploadItemIndex, activity, uploadItem);
},
throwable -> {
if (throwable instanceof UnknownHostException) {
view.showProgress(false);
view.showConnectionErrorPopup();
} else {
view.showMessage("" + throwable.getLocalizedMessage(),
R.color.color_error);
}
Timber.e(throwable, "Error occurred while handling image");
})
);
return true;
}
/**
* Stores the image quality in JSON format in SharedPrefs
*
* @param imageResult Image quality
* @param uploadItemIndex Index of the UploadItem whose quality is calculated
* @param activity Context reference
* @param uploadItem UploadItem whose quality is to be checked
*/
private void storeImageQuality(final Integer imageResult, final int uploadItemIndex, final Activity activity,
final UploadItem uploadItem) {
final BasicKvStore store = new BasicKvStore(activity,
UploadActivity.storeNameForCurrentUploadImagesSize);
final String value = store.getString(keyForCurrentUploadImageQualities, null);
final JSONObject jsonObject;
try {
if (value != null) {
jsonObject = new JSONObject(value);
} else {
jsonObject = new JSONObject();
}
jsonObject.put("UploadItem" + uploadItemIndex, imageResult);
store.putString(keyForCurrentUploadImageQualities, jsonObject.toString());
} catch (final Exception e) {
Timber.e(e);
}
if (uploadItemIndex == 0) {
if (!isBatteryDialogShowing && !isCategoriesDialogShowing) {
// if battery-optimisation dialog is not being shown, call checkImageQuality
checkImageQuality(uploadItem, uploadItemIndex);
} else {
view.showProgress(false);
}
}
}
/**
* 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
*/
@Override
public void checkImageQuality(final UploadItem uploadItem, final int index) {
if ((uploadItem.getImageQuality() != IMAGE_OK) && (uploadItem.getImageQuality()
!= IMAGE_KEEP)) {
final BasicKvStore store = new BasicKvStore(activity,
UploadActivity.storeNameForCurrentUploadImagesSize);
final String value = store.getString(keyForCurrentUploadImageQualities, null);
final JSONObject jsonObject;
try {
if (value != null) {
jsonObject = new JSONObject(value);
} else {
jsonObject = new JSONObject();
}
final Integer imageQuality = (int) jsonObject.get("UploadItem" + index);
view.showProgress(false);
if (imageQuality == IMAGE_OK) {
uploadItem.setHasInvalidLocation(false);
uploadItem.setImageQuality(imageQuality);
} else {
handleBadImage(imageQuality, uploadItem, index);
}
} catch (final Exception e) {
}
}
}
/**
* 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
*/
@Override
public void updateImageQualitiesJSON(final int size, final int index) {
final BasicKvStore store = new BasicKvStore(activity,
UploadActivity.storeNameForCurrentUploadImagesSize);
final String value = store.getString(keyForCurrentUploadImageQualities, null);
final JSONObject jsonObject;
try {
if (value != null) {
jsonObject = new JSONObject(value);
} else {
jsonObject = new JSONObject();
}
for (int i = index; i < (size - 1); i++) {
jsonObject.put("UploadItem" + i, jsonObject.get("UploadItem" + (i + 1)));
}
jsonObject.remove("UploadItem" + (size - 1));
store.putString(keyForCurrentUploadImageQualities, jsonObject.toString());
} catch (final Exception e) {
Timber.e(e);
}
}
/**
* Handles bad pictures, like too dark, already on wikimedia, downloaded from internet
*
* @param errorCode Error code of the bad image quality
* @param uploadItem UploadItem whose quality is bad
* @param index Index of item whose quality is bad
*/
public void handleBadImage(final Integer errorCode,
final UploadItem uploadItem, final int index) {
Timber.d("Handle bad picture with error code %d", errorCode);
if (errorCode >= 8) { // If location of image and nearby does not match
uploadItem.setHasInvalidLocation(true);
}
// If image has some other problems, show popup accordingly
if (errorCode != EMPTY_CAPTION && errorCode != FILE_NAME_EXISTS) {
showBadImagePopup(errorCode, index, activity, uploadItem);
}
}
/**
* Shows a dialog describing the potential problems in the current image
*
* @param errorCode Has the potential problems in the current image
* @param index Index of the UploadItem which has problems
* @param activity Context reference
* @param uploadItem UploadItem which has problems
*/
public void showBadImagePopup(final Integer errorCode,
final int index, final Activity activity, final UploadItem uploadItem) {
final String errorMessageForResult = getErrorMessageForResult(activity, errorCode);
if (!StringUtils.isBlank(errorMessageForResult)) {
DialogUtil.showAlertDialog(activity,
activity.getString(R.string.upload_problem_image),
errorMessageForResult,
activity.getString(R.string.upload),
activity.getString(R.string.cancel),
() -> {
view.showProgress(false);
uploadItem.setImageQuality(IMAGE_OK);
},
() -> {
presenterCallback.deletePictureAtIndex(index);
}
).setCancelable(false);
}
//If the error message is null, we will probably not show anything
}
/**
* notifies the user that a similar image exists
*/
@Override
public void showSimilarImageFragment(final String originalFilePath, final String possibleFilePath,
final ImageCoordinates similarImageCoordinates) {
view.showSimilarImageFragment(originalFilePath, possibleFilePath,
similarImageCoordinates
);
}
}

View file

@ -0,0 +1,472 @@
package fr.free.nrw.commons.upload.mediaDetails
import android.app.Activity
import fr.free.nrw.commons.R
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.filepicker.UploadableFile
import fr.free.nrw.commons.kvstore.BasicKvStore
import fr.free.nrw.commons.location.LatLng
import fr.free.nrw.commons.nearby.Place
import fr.free.nrw.commons.repository.UploadRepository
import fr.free.nrw.commons.upload.ImageCoordinates
import fr.free.nrw.commons.upload.SimilarImageInterface
import fr.free.nrw.commons.upload.UploadActivity
import fr.free.nrw.commons.upload.UploadActivity.Companion.setUploadIsOfAPlace
import fr.free.nrw.commons.upload.UploadItem
import fr.free.nrw.commons.upload.UploadMediaDetail
import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment.UploadMediaDetailFragmentCallback
import fr.free.nrw.commons.utils.DialogUtil.showAlertDialog
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_KEEP
import fr.free.nrw.commons.utils.ImageUtils.IMAGE_OK
import fr.free.nrw.commons.utils.ImageUtils.getErrorMessageForResult
import io.github.coordinates2country.Coordinates2Country
import io.reactivex.Maybe
import io.reactivex.Scheduler
import io.reactivex.disposables.CompositeDisposable
import org.json.JSONObject
import timber.log.Timber
import java.lang.reflect.Method
import java.lang.reflect.Proxy
import java.net.UnknownHostException
import java.util.Locale
import javax.inject.Inject
import javax.inject.Named
class UploadMediaPresenter @Inject constructor(
private val repository: UploadRepository,
@param:Named(IO_THREAD) private val ioScheduler: Scheduler,
@param:Named(MAIN_THREAD) private val mainThreadScheduler: Scheduler
) : UploadMediaDetailsContract.UserActionListener, SimilarImageInterface {
private var view = DUMMY
private val compositeDisposable = CompositeDisposable()
private val countryNamesAndCodes: Map<String, String> by lazy {
// Create a map containing all ISO countries 2-letter codes provided by
// `Locale.getISOCountries()` and their english names
buildMap {
Locale.getISOCountries().forEach {
put(Locale("en", it).getDisplayCountry(Locale.ENGLISH), it)
}
}
}
override fun onAttachView(view: UploadMediaDetailsContract.View) {
this.view = view
}
override fun onDetachView() {
view = DUMMY
compositeDisposable.clear()
}
/**
* Sets the Upload Media Details for the corresponding upload item
*/
override fun setUploadMediaDetails(
uploadMediaDetails: List<UploadMediaDetail>,
uploadItemIndex: Int
) {
repository.getUploads()[uploadItemIndex].uploadMediaDetails = uploadMediaDetails.toMutableList()
}
/**
* Receives the corresponding uploadable file, processes it and return the view with and uplaod item
*/
override fun receiveImage(
uploadableFile: UploadableFile?,
place: Place?,
inAppPictureLocation: LatLng?
) {
view.showProgress(true)
compositeDisposable.add(
repository.preProcessImage(
uploadableFile, place, this, inAppPictureLocation
).map { uploadItem: UploadItem ->
if (place != null && place.isMonument && place.location != null) {
val countryCode = countryNamesAndCodes[Coordinates2Country.country(
place.location.latitude,
place.location.longitude
)]
if (countryCode != null && WLM_SUPPORTED_COUNTRIES.contains(countryCode.lowercase())) {
uploadItem.isWLMUpload = true
uploadItem.countryCode = countryCode.lowercase()
}
}
uploadItem
}.subscribeOn(ioScheduler).observeOn(mainThreadScheduler)
.subscribe({ uploadItem: UploadItem ->
view.onImageProcessed(uploadItem)
view.updateMediaDetails(uploadItem.uploadMediaDetails)
view.showProgress(false)
val gpsCoords = uploadItem.gpsCoords
val hasImageCoordinates = gpsCoords != null && gpsCoords.imageCoordsExists
if (hasImageCoordinates && place == null) {
checkNearbyPlaces(uploadItem)
}
}, { throwable: Throwable? ->
Timber.e(throwable, "Error occurred in processing images")
})
)
}
/**
* This method checks for the nearest location that needs images and suggests it to the user.
*/
private fun checkNearbyPlaces(uploadItem: UploadItem) {
compositeDisposable.add(Maybe.fromCallable {
repository.checkNearbyPlaces(
uploadItem.gpsCoords!!.decLatitude, uploadItem.gpsCoords!!.decLongitude
)
}.subscribeOn(ioScheduler).observeOn(mainThreadScheduler).subscribe({
view.onNearbyPlaceFound(uploadItem, it)
}, { throwable: Throwable? ->
Timber.e(throwable, "Error occurred in processing images")
})
)
}
/**
* 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
*/
override fun displayLocDialog(
uploadItemIndex: Int, inAppPictureLocation: LatLng?,
hasUserRemovedLocation: Boolean
) {
val uploadItem = repository.getUploads()[uploadItemIndex]
if (uploadItem.gpsCoords!!.decimalCoords == null && inAppPictureLocation == null && !hasUserRemovedLocation) {
view.displayAddLocationDialog { verifyCaptionQuality(uploadItem) }
} else {
verifyCaptionQuality(uploadItem)
}
}
/**
* Verifies the image's caption and calls function to handle the result
*
* @param uploadItem UploadItem whose caption is checked
*/
private fun verifyCaptionQuality(uploadItem: UploadItem) {
view.showProgress(true)
compositeDisposable.add(repository.getCaptionQuality(uploadItem)
.observeOn(mainThreadScheduler)
.subscribe({ capResult: Int ->
view.showProgress(false)
handleCaptionResult(capResult, uploadItem)
}, { throwable: Throwable ->
view.showProgress(false)
if (throwable is UnknownHostException) {
view.showConnectionErrorPopupForCaptionCheck()
} else {
view.showMessage(throwable.localizedMessage, R.color.color_error)
}
Timber.e(throwable, "Error occurred while handling image")
})
)
}
/**
* Handles image's caption results and shows dialog if necessary
*
* @param errorCode Error code of the UploadItem
* @param uploadItem UploadItem whose caption is checked
*/
fun handleCaptionResult(errorCode: Int, uploadItem: UploadItem) {
// If errorCode is empty caption show message
if (errorCode == EMPTY_CAPTION) {
Timber.d("Captions are empty. Showing toast")
view.showMessage(R.string.add_caption_toast, R.color.color_error)
}
// If image with same file name exists check the bit in errorCode is set or not
if ((errorCode and FILE_NAME_EXISTS) != 0) {
Timber.d("Trying to show duplicate picture popup")
view.showDuplicatePicturePopup(uploadItem)
}
// If caption is not duplicate or user still wants to upload it
if (errorCode == IMAGE_OK) {
Timber.d("Image captions are okay or user still wants to upload it")
view.onImageValidationSuccess()
}
}
/**
* Copies the caption and description of the current item to the subsequent media
*/
override fun copyTitleAndDescriptionToSubsequentMedia(indexInViewFlipper: Int) {
for (i in indexInViewFlipper + 1 until repository.getCount()) {
val subsequentUploadItem = repository.getUploads()[i]
subsequentUploadItem.uploadMediaDetails = deepCopy(
repository.getUploads()[indexInViewFlipper].uploadMediaDetails
).toMutableList()
}
}
/**
* Fetches and set the caption and description of the item
*/
override fun fetchTitleAndDescription(indexInViewFlipper: Int) =
view.updateMediaDetails(repository.getUploads()[indexInViewFlipper].uploadMediaDetails)
private fun deepCopy(uploadMediaDetails: List<UploadMediaDetail>) =
uploadMediaDetails.map(UploadMediaDetail::javaCopy)
override fun useSimilarPictureCoordinates(
imageCoordinates: ImageCoordinates, uploadItemIndex: Int
) = repository.useSimilarPictureCoordinates(imageCoordinates, uploadItemIndex)
override fun onMapIconClicked(indexInViewFlipper: Int) =
view.showExternalMap(repository.getUploads()[indexInViewFlipper])
override fun onEditButtonClicked(indexInViewFlipper: Int) =
view.showEditActivity(repository.getUploads()[indexInViewFlipper])
/**
* Updates the information regarding the specified place for the specified upload item
* when the user confirms the suggested nearby place.
*
* @param place The place to be associated with the uploads.
* @param uploadItemIndex Index of the uploadItem whose detected place has been confirmed
*/
override fun onUserConfirmedUploadIsOfPlace(place: Place?, uploadItemIndex: Int) {
val uploadItem = repository.getUploads()[uploadItemIndex]
uploadItem.place = place
val uploadMediaDetails = uploadItem.uploadMediaDetails
// Update UploadMediaDetail object for this UploadItem
uploadMediaDetails[0] = UploadMediaDetail(place)
// Now that the UploadItem and its associated UploadMediaDetail objects have been updated,
// update the view with the modified media details of the first upload item
view.updateMediaDetails(uploadMediaDetails)
setUploadIsOfAPlace(true)
}
/**
* 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
*/
override fun getImageQuality(
uploadItemIndex: Int,
inAppPictureLocation: LatLng?,
activity: Activity
): Boolean {
val uploadItems = repository.getUploads()
view.showProgress(true)
if (uploadItems.isEmpty()) {
view.showProgress(false)
// No internationalization required for this error message because it's an internal error.
view.showMessage(
"Internal error: Zero upload items received by the Upload Media Detail Fragment. Sorry, please upload again.",
R.color.color_error
)
return false
}
val uploadItem = uploadItems[uploadItemIndex]
compositeDisposable.add(repository.getImageQuality(uploadItem, inAppPictureLocation)
.observeOn(mainThreadScheduler)
.subscribe({ imageResult: Int ->
storeImageQuality(imageResult, uploadItemIndex, activity, uploadItem)
}, { throwable: Throwable ->
if (throwable is UnknownHostException) {
view.showProgress(false)
view.showConnectionErrorPopup()
} else {
view.showMessage(throwable.localizedMessage, R.color.color_error)
}
Timber.e(throwable, "Error occurred while handling image")
})
)
return true
}
/**
* Stores the image quality in JSON format in SharedPrefs
*
* @param imageResult Image quality
* @param uploadItemIndex Index of the UploadItem whose quality is calculated
* @param activity Context reference
* @param uploadItem UploadItem whose quality is to be checked
*/
private fun storeImageQuality(
imageResult: Int, uploadItemIndex: Int, activity: Activity, uploadItem: UploadItem
) {
val store = BasicKvStore(activity, UploadActivity.storeNameForCurrentUploadImagesSize)
val value = store.getString(UPLOAD_QUALITIES_KEY, null)
try {
val jsonObject = value.asJsonObject().apply {
put("UploadItem$uploadItemIndex", imageResult)
}
store.putString(UPLOAD_QUALITIES_KEY, jsonObject.toString())
} catch (e: Exception) {
Timber.e(e)
}
if (uploadItemIndex == 0) {
if (!isBatteryDialogShowing && !isCategoriesDialogShowing) {
// if battery-optimisation dialog is not being shown, call checkImageQuality
checkImageQuality(uploadItem, uploadItemIndex)
} else {
view.showProgress(false)
}
}
}
/**
* 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
*/
override fun checkImageQuality(uploadItem: UploadItem, index: Int) {
if ((uploadItem.imageQuality != IMAGE_OK) && (uploadItem.imageQuality != IMAGE_KEEP)) {
val store = BasicKvStore(
UploadMediaDetailFragment.activity,
UploadActivity.storeNameForCurrentUploadImagesSize
)
val value = store.getString(UPLOAD_QUALITIES_KEY, null)
try {
val imageQuality = value.asJsonObject()["UploadItem$index"] as Int
view.showProgress(false)
if (imageQuality == IMAGE_OK) {
uploadItem.hasInvalidLocation = false
uploadItem.imageQuality = imageQuality
} else {
handleBadImage(imageQuality, uploadItem, index)
}
} catch (e: Exception) {
Timber.e(e)
}
}
}
/**
* 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
*/
override fun updateImageQualitiesJSON(size: Int, index: Int) {
val store = BasicKvStore(
UploadMediaDetailFragment.activity,
UploadActivity.storeNameForCurrentUploadImagesSize
)
val value = store.getString(UPLOAD_QUALITIES_KEY, null)
try {
val jsonObject = value.asJsonObject().apply {
for (i in index until (size - 1)) {
put("UploadItem$i", this["UploadItem" + (i + 1)])
}
remove("UploadItem" + (size - 1))
}
store.putString(UPLOAD_QUALITIES_KEY, jsonObject.toString())
} catch (e: Exception) {
Timber.e(e)
}
}
/**
* Handles bad pictures, like too dark, already on wikimedia, downloaded from internet
*
* @param errorCode Error code of the bad image quality
* @param uploadItem UploadItem whose quality is bad
* @param index Index of item whose quality is bad
*/
private fun handleBadImage(
errorCode: Int,
uploadItem: UploadItem, index: Int
) {
Timber.d("Handle bad picture with error code %d", errorCode)
if (errorCode >= 8) { // If location of image and nearby does not match
uploadItem.hasInvalidLocation = true
}
// If image has some other problems, show popup accordingly
if (errorCode != EMPTY_CAPTION && errorCode != FILE_NAME_EXISTS) {
showBadImagePopup(errorCode, index, UploadMediaDetailFragment.activity, uploadItem)
}
}
/**
* Shows a dialog describing the potential problems in the current image
*
* @param errorCode Has the potential problems in the current image
* @param index Index of the UploadItem which has problems
* @param activity Context reference
* @param uploadItem UploadItem which has problems
*/
private fun showBadImagePopup(
errorCode: Int, index: Int, activity: Activity, uploadItem: UploadItem
) {
//If the error message is null, we will probably not show anything
val errorMessageForResult = getErrorMessageForResult(activity, errorCode)
if (errorMessageForResult.isNotEmpty()) {
showAlertDialog(activity,
activity.getString(R.string.upload_problem_image),
errorMessageForResult,
activity.getString(R.string.upload),
activity.getString(R.string.cancel),
{
view.showProgress(false)
uploadItem.imageQuality = IMAGE_OK
}, {
presenterCallback!!.deletePictureAtIndex(index)
}
)?.setCancelable(false)
}
}
/**
* notifies the user that a similar image exists
*/
override fun showSimilarImageFragment(
originalFilePath: String?,
possibleFilePath: String?,
similarImageCoordinates: ImageCoordinates?
) = view.showSimilarImageFragment(originalFilePath, possibleFilePath, similarImageCoordinates)
private fun String?.asJsonObject() = if (this != null) {
JSONObject(this)
} else {
JSONObject()
}
companion object {
private const val UPLOAD_QUALITIES_KEY = "UploadedImagesQualities"
private val WLM_SUPPORTED_COUNTRIES = listOf(
"am", "at", "az", "br", "hr", "sv", "fi", "fr", "de", "gh",
"in", "ie", "il", "mk", "my", "mt", "pk", "pe", "pl", "ru",
"rw", "si", "es", "se", "tw", "ug", "ua", "us"
)
private val DUMMY = Proxy.newProxyInstance(
UploadMediaDetailsContract.View::class.java.classLoader,
arrayOf<Class<*>>(UploadMediaDetailsContract.View::class.java)
) { _: Any?, _: Method?, _: Array<Any?>? -> null } as UploadMediaDetailsContract.View
var presenterCallback: UploadMediaDetailFragmentCallback? = null
/**
* Variable used to determine if the battery-optimisation dialog is being shown or not
*/
var isBatteryDialogShowing: Boolean = false
var isCategoriesDialogShowing: Boolean = false
}
}

View file

@ -1,10 +1,12 @@
package fr.free.nrw.commons.upload package fr.free.nrw.commons.upload
import android.net.Uri import android.net.Uri
import com.nhaarman.mockitokotlin2.argumentCaptor
import com.nhaarman.mockitokotlin2.isA
import com.nhaarman.mockitokotlin2.mock import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.whenever import com.nhaarman.mockitokotlin2.whenever
import fr.free.nrw.commons.R
import fr.free.nrw.commons.filepicker.UploadableFile import fr.free.nrw.commons.filepicker.UploadableFile
import fr.free.nrw.commons.kvstore.JsonKvStore
import fr.free.nrw.commons.location.LatLng import fr.free.nrw.commons.location.LatLng
import fr.free.nrw.commons.nearby.Place import fr.free.nrw.commons.nearby.Place
import fr.free.nrw.commons.repository.UploadRepository import fr.free.nrw.commons.repository.UploadRepository
@ -24,6 +26,7 @@ import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor import org.mockito.ArgumentCaptor
import org.mockito.ArgumentMatchers import org.mockito.ArgumentMatchers
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.Mock import org.mockito.Mock
import org.mockito.MockedStatic import org.mockito.MockedStatic
import org.mockito.Mockito import org.mockito.Mockito
@ -55,7 +58,7 @@ class UploadMediaPresenterTest {
private lateinit var place: Place private lateinit var place: Place
@Mock @Mock
private var location: LatLng? = null private lateinit var location: LatLng
@Mock @Mock
private lateinit var uploadItem: UploadItem private lateinit var uploadItem: UploadItem
@ -63,18 +66,12 @@ class UploadMediaPresenterTest {
@Mock @Mock
private lateinit var imageCoordinates: ImageCoordinates private lateinit var imageCoordinates: ImageCoordinates
@Mock
private lateinit var uploadMediaDetails: List<UploadMediaDetail>
private lateinit var testObservableUploadItem: Observable<UploadItem> private lateinit var testObservableUploadItem: Observable<UploadItem>
private lateinit var testSingleImageResult: Single<Int> private lateinit var testSingleImageResult: Single<Int>
private lateinit var testScheduler: TestScheduler private lateinit var testScheduler: TestScheduler
private lateinit var mockedCountry: MockedStatic<Coordinates2Country> private lateinit var mockedCountry: MockedStatic<Coordinates2Country>
@Mock
private lateinit var jsonKvStore: JsonKvStore
@Mock @Mock
lateinit var mockActivity: UploadActivity lateinit var mockActivity: UploadActivity
@ -91,7 +88,6 @@ class UploadMediaPresenterTest {
uploadMediaPresenter = uploadMediaPresenter =
UploadMediaPresenter( UploadMediaPresenter(
repository, repository,
jsonKvStore,
testScheduler, testScheduler,
testScheduler, testScheduler,
) )
@ -120,10 +116,7 @@ class UploadMediaPresenterTest {
uploadMediaPresenter.receiveImage(uploadableFile, place, location) uploadMediaPresenter.receiveImage(uploadableFile, place, location)
verify(view).showProgress(true) verify(view).showProgress(true)
testScheduler.triggerActions() testScheduler.triggerActions()
verify(view).onImageProcessed( verify(view).onImageProcessed(isA())
ArgumentMatchers.any(UploadItem::class.java),
ArgumentMatchers.any(Place::class.java),
)
} }
/** /**
@ -167,7 +160,7 @@ class UploadMediaPresenterTest {
@Test @Test
fun emptyFileNameTest() { fun emptyFileNameTest() {
uploadMediaPresenter.handleCaptionResult(EMPTY_CAPTION, uploadItem) uploadMediaPresenter.handleCaptionResult(EMPTY_CAPTION, uploadItem)
verify(view).showMessage(ArgumentMatchers.anyInt(), ArgumentMatchers.anyInt()) verify(view).showMessage(R.string.add_caption_toast, R.color.color_error)
} }
/** /**
@ -226,12 +219,11 @@ class UploadMediaPresenterTest {
@Test @Test
fun fetchImageAndTitleTest() { fun fetchImageAndTitleTest() {
whenever(repository.getUploads()).thenReturn(listOf(uploadItem)) whenever(repository.getUploads()).thenReturn(listOf(uploadItem))
whenever(repository.getUploadItem(ArgumentMatchers.anyInt())) whenever(repository.getUploadItem(ArgumentMatchers.anyInt())).thenReturn(uploadItem)
.thenReturn(uploadItem)
whenever(uploadItem.uploadMediaDetails).thenReturn(mutableListOf()) whenever(uploadItem.uploadMediaDetails).thenReturn(mutableListOf())
uploadMediaPresenter.fetchTitleAndDescription(0) uploadMediaPresenter.fetchTitleAndDescription(0)
verify(view).updateMediaDetails(ArgumentMatchers.any()) verify(view).updateMediaDetails(isA())
} }
/** /**
@ -273,12 +265,9 @@ class UploadMediaPresenterTest {
verify(view).showProgress(true) verify(view).showProgress(true)
testScheduler.triggerActions() testScheduler.triggerActions()
val captor: ArgumentCaptor<UploadItem> = ArgumentCaptor.forClass(UploadItem::class.java) val captor = argumentCaptor<UploadItem>()
verify(view).onImageProcessed( verify(view).onImageProcessed(captor.capture())
captor.capture(),
ArgumentMatchers.any(Place::class.java),
)
assertEquals("Exptected contry code", "de", captor.value.countryCode) assertEquals("Exptected contry code", "de", captor.firstValue.countryCode)
} }
} }

View file

@ -100,7 +100,7 @@ class UploadMediaDetailFragmentUnitTest {
private lateinit var place: Place private lateinit var place: Place
@Mock @Mock
private var location: fr.free.nrw.commons.location.LatLng? = null private lateinit var location: LatLng
@Mock @Mock
private lateinit var defaultKvStore: JsonKvStore private lateinit var defaultKvStore: JsonKvStore
@ -194,7 +194,7 @@ class UploadMediaDetailFragmentUnitTest {
Whitebox.setInternalState(fragment, "presenter", presenter) Whitebox.setInternalState(fragment, "presenter", presenter)
val method: Method = val method: Method =
UploadMediaDetailFragment::class.java.getDeclaredMethod( UploadMediaDetailFragment::class.java.getDeclaredMethod(
"init", "initializeFragment",
) )
method.isAccessible = true method.isAccessible = true
method.invoke(fragment) method.invoke(fragment)
@ -209,7 +209,7 @@ class UploadMediaDetailFragmentUnitTest {
`when`(callback.totalNumberOfSteps).thenReturn(5) `when`(callback.totalNumberOfSteps).thenReturn(5)
val method: Method = val method: Method =
UploadMediaDetailFragment::class.java.getDeclaredMethod( UploadMediaDetailFragment::class.java.getDeclaredMethod(
"init", "initializeFragment",
) )
method.isAccessible = true method.isAccessible = true
method.invoke(fragment) method.invoke(fragment)
@ -258,7 +258,7 @@ class UploadMediaDetailFragmentUnitTest {
fun testOnImageProcessed() { fun testOnImageProcessed() {
Shadows.shadowOf(Looper.getMainLooper()).idle() Shadows.shadowOf(Looper.getMainLooper()).idle()
`when`(uploadItem.mediaUri).thenReturn(mediaUri) `when`(uploadItem.mediaUri).thenReturn(mediaUri)
fragment.onImageProcessed(uploadItem, place) fragment.onImageProcessed(uploadItem)
} }
@Test @Test
@ -407,7 +407,7 @@ class UploadMediaDetailFragmentUnitTest {
@Throws(Exception::class) @Throws(Exception::class)
fun testUpdateMediaDetails() { fun testUpdateMediaDetails() {
Shadows.shadowOf(Looper.getMainLooper()).idle() Shadows.shadowOf(Looper.getMainLooper()).idle()
fragment.updateMediaDetails(null) fragment.updateMediaDetails(mock())
} }
@Test @Test