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

View file

@ -175,11 +175,11 @@ class UploadModel @Inject internal constructor(
Timber.d(
"Created timestamp while building contribution is %s, %s",
item.createdTimestamp,
Date(item.createdTimestamp!!)
item.createdTimestamp?.let { Date(it) }
)
if (item.createdTimestamp != -1L) {
contribution.dateCreated = Date(item.createdTimestamp)
contribution.dateCreated = item.createdTimestamp?.let { Date(it) }
contribution.dateCreatedSource = item.createdTimestampSource
//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
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.R
import fr.free.nrw.commons.contributions.Contribution
@ -69,8 +68,7 @@ class UploadPresenter @Inject internal constructor(
private fun processContributionsForSubmission() {
if (view.isLoggedIn()) {
view.showProgress(true)
repository.buildContributions()
?.observeOn(Schedulers.io())
repository.buildContributions().observeOn(Schedulers.io())
?.subscribe(object : Observer<Contribution> {
override fun onSubscribe(d: Disposable) {
view.showProgress(false)
@ -133,8 +131,9 @@ class UploadPresenter @Inject internal constructor(
* @param uploadItemIndex Index of next image, whose quality is to be checked
*/
override fun checkImageQuality(uploadItemIndex: Int) {
val uploadItem = repository.getUploadItem(uploadItemIndex)
presenter.checkImageQuality(uploadItem, uploadItemIndex)
repository.getUploadItem(uploadItemIndex)?.let {
presenter.checkImageQuality(it, uploadItemIndex)
}
}
override fun deletePictureAtIndex(index: Int) {
@ -156,8 +155,9 @@ class UploadPresenter @Inject internal constructor(
view.onUploadMediaDeleted(index)
if (index != uploadableFiles.size && index != 0) {
// if the deleted image was not the last item to be uploaded, check quality of next
val uploadItem = repository.getUploadItem(index)
presenter.checkImageQuality(uploadItem, index)
repository.getUploadItem(index)?.let {
presenter.checkImageQuality(it, index)
}
}
if (uploadableFiles.size < 2) {

View file

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

View file

@ -15,9 +15,9 @@ import fr.free.nrw.commons.upload.UploadMediaDetail
*/
interface UploadMediaDetailsContract {
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)
@ -27,7 +27,7 @@ interface UploadMediaDetailsContract {
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
@ -42,13 +42,13 @@ interface UploadMediaDetailsContract {
*/
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?> {
@ -59,7 +59,7 @@ interface UploadMediaDetailsContract {
)
fun setUploadMediaDetails(
uploadMediaDetails: List<UploadMediaDetail?>?,
uploadMediaDetails: List<UploadMediaDetail>,
uploadItemIndex: Int
)
@ -74,7 +74,7 @@ interface UploadMediaDetailsContract {
fun getImageQuality(
uploadItemIndex: Int,
inAppPictureLocation: LatLng?,
activity: Activity?
activity: Activity
): Boolean
/**
@ -87,7 +87,8 @@ interface UploadMediaDetailsContract {
* @param hasUserRemovedLocation True if user has removed location from the image
*/
fun displayLocDialog(
uploadItemIndex: Int, inAppPictureLocation: LatLng?,
uploadItemIndex: Int,
inAppPictureLocation: LatLng?,
hasUserRemovedLocation: Boolean
)
@ -97,7 +98,7 @@ interface UploadMediaDetailsContract {
* @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)
fun checkImageQuality(uploadItem: UploadItem, index: Int)
/**
* Updates the image qualities stored in JSON, whenever an image is deleted
@ -111,7 +112,7 @@ interface UploadMediaDetailsContract {
fun fetchTitleAndDescription(indexInViewFlipper: Int)
fun useSimilarPictureCoordinates(imageCoordinates: ImageCoordinates?, uploadItemIndex: Int)
fun useSimilarPictureCoordinates(imageCoordinates: ImageCoordinates, uploadItemIndex: 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
import android.net.Uri
import com.nhaarman.mockitokotlin2.argumentCaptor
import com.nhaarman.mockitokotlin2.isA
import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.whenever
import fr.free.nrw.commons.R
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.nearby.Place
import fr.free.nrw.commons.repository.UploadRepository
@ -24,6 +26,7 @@ import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
import org.mockito.ArgumentMatchers
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.Mock
import org.mockito.MockedStatic
import org.mockito.Mockito
@ -55,7 +58,7 @@ class UploadMediaPresenterTest {
private lateinit var place: Place
@Mock
private var location: LatLng? = null
private lateinit var location: LatLng
@Mock
private lateinit var uploadItem: UploadItem
@ -63,18 +66,12 @@ class UploadMediaPresenterTest {
@Mock
private lateinit var imageCoordinates: ImageCoordinates
@Mock
private lateinit var uploadMediaDetails: List<UploadMediaDetail>
private lateinit var testObservableUploadItem: Observable<UploadItem>
private lateinit var testSingleImageResult: Single<Int>
private lateinit var testScheduler: TestScheduler
private lateinit var mockedCountry: MockedStatic<Coordinates2Country>
@Mock
private lateinit var jsonKvStore: JsonKvStore
@Mock
lateinit var mockActivity: UploadActivity
@ -91,7 +88,6 @@ class UploadMediaPresenterTest {
uploadMediaPresenter =
UploadMediaPresenter(
repository,
jsonKvStore,
testScheduler,
testScheduler,
)
@ -120,10 +116,7 @@ class UploadMediaPresenterTest {
uploadMediaPresenter.receiveImage(uploadableFile, place, location)
verify(view).showProgress(true)
testScheduler.triggerActions()
verify(view).onImageProcessed(
ArgumentMatchers.any(UploadItem::class.java),
ArgumentMatchers.any(Place::class.java),
)
verify(view).onImageProcessed(isA())
}
/**
@ -167,7 +160,7 @@ class UploadMediaPresenterTest {
@Test
fun emptyFileNameTest() {
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
fun fetchImageAndTitleTest() {
whenever(repository.getUploads()).thenReturn(listOf(uploadItem))
whenever(repository.getUploadItem(ArgumentMatchers.anyInt()))
.thenReturn(uploadItem)
whenever(repository.getUploadItem(ArgumentMatchers.anyInt())).thenReturn(uploadItem)
whenever(uploadItem.uploadMediaDetails).thenReturn(mutableListOf())
uploadMediaPresenter.fetchTitleAndDescription(0)
verify(view).updateMediaDetails(ArgumentMatchers.any())
verify(view).updateMediaDetails(isA())
}
/**
@ -273,12 +265,9 @@ class UploadMediaPresenterTest {
verify(view).showProgress(true)
testScheduler.triggerActions()
val captor: ArgumentCaptor<UploadItem> = ArgumentCaptor.forClass(UploadItem::class.java)
verify(view).onImageProcessed(
captor.capture(),
ArgumentMatchers.any(Place::class.java),
)
val captor = argumentCaptor<UploadItem>()
verify(view).onImageProcessed(captor.capture())
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
@Mock
private var location: fr.free.nrw.commons.location.LatLng? = null
private lateinit var location: LatLng
@Mock
private lateinit var defaultKvStore: JsonKvStore
@ -194,7 +194,7 @@ class UploadMediaDetailFragmentUnitTest {
Whitebox.setInternalState(fragment, "presenter", presenter)
val method: Method =
UploadMediaDetailFragment::class.java.getDeclaredMethod(
"init",
"initializeFragment",
)
method.isAccessible = true
method.invoke(fragment)
@ -209,7 +209,7 @@ class UploadMediaDetailFragmentUnitTest {
`when`(callback.totalNumberOfSteps).thenReturn(5)
val method: Method =
UploadMediaDetailFragment::class.java.getDeclaredMethod(
"init",
"initializeFragment",
)
method.isAccessible = true
method.invoke(fragment)
@ -258,7 +258,7 @@ class UploadMediaDetailFragmentUnitTest {
fun testOnImageProcessed() {
Shadows.shadowOf(Looper.getMainLooper()).idle()
`when`(uploadItem.mediaUri).thenReturn(mediaUri)
fragment.onImageProcessed(uploadItem, place)
fragment.onImageProcessed(uploadItem)
}
@Test
@ -407,7 +407,7 @@ class UploadMediaDetailFragmentUnitTest {
@Throws(Exception::class)
fun testUpdateMediaDetails() {
Shadows.shadowOf(Looper.getMainLooper()).idle()
fragment.updateMediaDetails(null)
fragment.updateMediaDetails(mock())
}
@Test