diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/Contribution.kt b/app/src/main/java/fr/free/nrw/commons/contributions/Contribution.kt
index 0a757619f0..84ce0eb9c6 100644
--- a/app/src/main/java/fr/free/nrw/commons/contributions/Contribution.kt
+++ b/app/src/main/java/fr/free/nrw/commons/contributions/Contribution.kt
@@ -63,13 +63,13 @@ data class Contribution constructor(
Media(
formatCaptions(item.uploadMediaDetails),
categories,
- item.fileName,
+ item.filename,
formatDescriptions(item.uploadMediaDetails),
sessionManager.userName,
sessionManager.userName,
),
localUri = item.mediaUri,
- decimalCoords = item.gpsCoords.decimalCoords,
+ decimalCoords = item.gpsCoords?.decimalCoords,
dateCreatedSource = "",
depictedItems = depictedItems,
wikidataPlace = from(item.place),
diff --git a/app/src/main/java/fr/free/nrw/commons/filepicker/MimeTypeMapWrapper.kt b/app/src/main/java/fr/free/nrw/commons/filepicker/MimeTypeMapWrapper.kt
index 0cf21cc027..5fe4c288bd 100644
--- a/app/src/main/java/fr/free/nrw/commons/filepicker/MimeTypeMapWrapper.kt
+++ b/app/src/main/java/fr/free/nrw/commons/filepicker/MimeTypeMapWrapper.kt
@@ -13,7 +13,7 @@ class MimeTypeMapWrapper {
)
@JvmStatic
- fun getExtensionFromMimeType(mimeType: String): String? {
+ fun getExtensionFromMimeType(mimeType: String?): String? {
val result = sMimeTypeToExtensionMap[mimeType]
if (result != null) {
return result
diff --git a/app/src/main/java/fr/free/nrw/commons/repository/UploadRepository.kt b/app/src/main/java/fr/free/nrw/commons/repository/UploadRepository.kt
index 3779532547..f679960b9f 100644
--- a/app/src/main/java/fr/free/nrw/commons/repository/UploadRepository.kt
+++ b/app/src/main/java/fr/free/nrw/commons/repository/UploadRepository.kt
@@ -243,7 +243,7 @@ class UploadRepository @Inject constructor(
*
* @param licenseName
*/
- fun setSelectedLicense(licenseName: String) {
+ fun setSelectedLicense(licenseName: String?) {
uploadModel.selectedLicense = licenseName
}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/ImageProcessingService.kt b/app/src/main/java/fr/free/nrw/commons/upload/ImageProcessingService.kt
index 9fbb1f1e44..ba9dc147c1 100644
--- a/app/src/main/java/fr/free/nrw/commons/upload/ImageProcessingService.kt
+++ b/app/src/main/java/fr/free/nrw/commons/upload/ImageProcessingService.kt
@@ -45,7 +45,7 @@ class ImageProcessingService @Inject constructor(
}
Timber.d("Checking the validity of image")
- val filePath = uploadItem.mediaUri.path
+ val filePath = uploadItem.mediaUri?.path
return Single.zip(
checkDuplicateImage(filePath),
@@ -107,7 +107,7 @@ class ImageProcessingService @Inject constructor(
return Single.just(EMPTY_CAPTION)
}
- return mediaClient.checkPageExistsUsingTitle("File:" + uploadItem.fileName)
+ return mediaClient.checkPageExistsUsingTitle("File:" + uploadItem.filename)
.map { doesFileExist: Boolean ->
Timber.d("Result for valid title is %s", doesFileExist)
if (doesFileExist) FILE_NAME_EXISTS else IMAGE_OK
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/SimilarImageDialogFragment.java b/app/src/main/java/fr/free/nrw/commons/upload/SimilarImageDialogFragment.java
deleted file mode 100644
index c1db2fd4fc..0000000000
--- a/app/src/main/java/fr/free/nrw/commons/upload/SimilarImageDialogFragment.java
+++ /dev/null
@@ -1,109 +0,0 @@
-package fr.free.nrw.commons.upload;
-
-import android.app.Dialog;
-import android.content.DialogInterface;
-import android.net.Uri;
-import android.os.Bundle;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.Window;
-import androidx.annotation.Nullable;
-import androidx.fragment.app.DialogFragment;
-import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat;
-import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder;
-import fr.free.nrw.commons.R;
-import fr.free.nrw.commons.databinding.FragmentSimilarImageDialogBinding;
-import java.io.File;
-
-/**
- * Created by harisanker on 14/2/18.
- */
-
-public class SimilarImageDialogFragment extends DialogFragment {
-
- Callback callback;//Implemented interface from shareActivity
- Boolean gotResponse = false;
-
- private FragmentSimilarImageDialogBinding binding;
-
- public SimilarImageDialogFragment() {
- }
- public interface Callback {
- void onPositiveResponse();
-
- void onNegativeResponse();
- }
-
- public void setCallback(Callback callback) {
- this.callback = callback;
- }
-
- @Override
- public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
- binding = FragmentSimilarImageDialogBinding.inflate(inflater, container, false);
-
-
- binding.orginalImage.setHierarchy(GenericDraweeHierarchyBuilder
- .newInstance(getResources())
- .setPlaceholderImage(VectorDrawableCompat.create(getResources(),
- R.drawable.ic_image_black_24dp,getContext().getTheme()))
- .setFailureImage(VectorDrawableCompat.create(getResources(),
- R.drawable.ic_error_outline_black_24dp, getContext().getTheme()))
- .build());
- binding.possibleImage.setHierarchy(GenericDraweeHierarchyBuilder
- .newInstance(getResources())
- .setPlaceholderImage(VectorDrawableCompat.create(getResources(),
- R.drawable.ic_image_black_24dp,getContext().getTheme()))
- .setFailureImage(VectorDrawableCompat.create(getResources(),
- R.drawable.ic_error_outline_black_24dp, getContext().getTheme()))
- .build());
-
- binding.orginalImage.setImageURI(Uri.fromFile(new File(getArguments().getString("originalImagePath"))));
- binding.possibleImage.setImageURI(Uri.fromFile(new File(getArguments().getString("possibleImagePath"))));
-
- binding.postiveButton.setOnClickListener(v -> onPositiveButtonClicked());
- binding.negativeButton.setOnClickListener(v -> onNegativeButtonClicked());
-
- return binding.getRoot();
- }
-
- @Override
- public void onCreate(@Nullable Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- }
-
- @Override
- public Dialog onCreateDialog(Bundle savedInstanceState) {
- Dialog dialog = super.onCreateDialog(savedInstanceState);
- dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
- return dialog;
- }
-
- @Override
- public void onDismiss(DialogInterface dialog) {
-// I user dismisses dialog by pressing outside the dialog.
- if (!gotResponse) {
- callback.onNegativeResponse();
- }
- super.onDismiss(dialog);
- }
-
- public void onNegativeButtonClicked() {
- callback.onNegativeResponse();
- gotResponse = true;
- dismiss();
- }
-
- public void onPositiveButtonClicked() {
- callback.onPositiveResponse();
- gotResponse = true;
- dismiss();
- }
-
- @Override
- public void onDestroy() {
- super.onDestroy();
- binding = null;
- }
-}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/SimilarImageDialogFragment.kt b/app/src/main/java/fr/free/nrw/commons/upload/SimilarImageDialogFragment.kt
new file mode 100644
index 0000000000..c5d82ed107
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/upload/SimilarImageDialogFragment.kt
@@ -0,0 +1,105 @@
+package fr.free.nrw.commons.upload
+
+import android.app.Dialog
+import android.content.DialogInterface
+import android.net.Uri
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.view.Window
+import androidx.fragment.app.DialogFragment
+import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat
+import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder
+import fr.free.nrw.commons.R
+import fr.free.nrw.commons.databinding.FragmentSimilarImageDialogBinding
+import java.io.File
+
+/**
+ * Created by harisanker on 14/2/18.
+ */
+class SimilarImageDialogFragment : DialogFragment() {
+ var callback: Callback? = null //Implemented interface from shareActivity
+ var gotResponse: Boolean = false
+
+ private var _binding: FragmentSimilarImageDialogBinding? = null
+ private val binding: FragmentSimilarImageDialogBinding get() = _binding!!
+
+ interface Callback {
+ fun onPositiveResponse()
+
+ fun onNegativeResponse()
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ _binding = FragmentSimilarImageDialogBinding.inflate(inflater, container, false)
+
+ binding.orginalImage.hierarchy =
+ GenericDraweeHierarchyBuilder.newInstance(resources).setPlaceholderImage(
+ VectorDrawableCompat.create(
+ resources, R.drawable.ic_image_black_24dp, requireContext().theme
+ )
+ ).setFailureImage(
+ VectorDrawableCompat.create(
+ resources, R.drawable.ic_error_outline_black_24dp, requireContext().theme
+ )
+ ).build()
+
+ binding.possibleImage.hierarchy =
+ GenericDraweeHierarchyBuilder.newInstance(resources).setPlaceholderImage(
+ VectorDrawableCompat.create(
+ resources, R.drawable.ic_image_black_24dp, requireContext().theme
+ )
+ ).setFailureImage(
+ VectorDrawableCompat.create(
+ resources, R.drawable.ic_error_outline_black_24dp, requireContext().theme
+ )
+ ).build()
+
+ arguments?.let {
+ binding.orginalImage.setImageURI(
+ Uri.fromFile(File(it.getString("originalImagePath")!!))
+ )
+ binding.possibleImage.setImageURI(
+ Uri.fromFile(File(it.getString("possibleImagePath")!!))
+ )
+ }
+
+ binding.postiveButton.setOnClickListener {
+ callback?.onPositiveResponse()
+ gotResponse = true
+ dismiss()
+ }
+
+ binding.negativeButton.setOnClickListener {
+ callback?.onNegativeResponse()
+ gotResponse = true
+ dismiss()
+ }
+
+ return binding.root
+ }
+
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ val dialog = super.onCreateDialog(savedInstanceState)
+ dialog.requestWindowFeature(Window.FEATURE_NO_TITLE)
+ return dialog
+ }
+
+ override fun onDismiss(dialog: DialogInterface) {
+ // I user dismisses dialog by pressing outside the dialog.
+ if (!gotResponse) {
+ callback?.onNegativeResponse()
+ }
+ super.onDismiss(dialog)
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ _binding = null
+ }
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/ThumbnailsAdapter.java b/app/src/main/java/fr/free/nrw/commons/upload/ThumbnailsAdapter.java
deleted file mode 100644
index 5975cbb1dd..0000000000
--- a/app/src/main/java/fr/free/nrw/commons/upload/ThumbnailsAdapter.java
+++ /dev/null
@@ -1,141 +0,0 @@
-package fr.free.nrw.commons.upload;
-
-import android.content.Context;
-import android.graphics.drawable.GradientDrawable;
-import android.net.Uri;
-import android.os.Build.VERSION;
-import android.os.Build.VERSION_CODES;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.ImageView;
-import android.widget.RelativeLayout;
-import androidx.annotation.NonNull;
-import androidx.recyclerview.widget.RecyclerView;
-import com.facebook.drawee.view.SimpleDraweeView;
-import fr.free.nrw.commons.R;
-import fr.free.nrw.commons.databinding.ItemUploadThumbnailBinding;
-import fr.free.nrw.commons.filepicker.UploadableFile;
-import java.io.File;
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * The adapter class for image thumbnails to be shown while uploading.
- */
-class ThumbnailsAdapter extends RecyclerView.Adapter {
- public static Context context;
- List uploadableFiles;
- private Callback callback;
-
- private OnThumbnailDeletedListener listener;
-
- private ItemUploadThumbnailBinding binding;
-
- public ThumbnailsAdapter(Callback callback) {
- this.uploadableFiles = new ArrayList<>();
- this.callback = callback;
- }
-
- /**
- * Sets the data, the media files
- * @param uploadableFiles
- */
- public void setUploadableFiles(
- List uploadableFiles) {
- this.uploadableFiles=uploadableFiles;
- notifyDataSetChanged();
- }
-
- public void setOnThumbnailDeletedListener(OnThumbnailDeletedListener listener) {
- this.listener = listener;
- }
-
- @NonNull
- @Override
- public ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
- binding = ItemUploadThumbnailBinding.inflate(LayoutInflater.from(viewGroup.getContext()), viewGroup, false);
- return new ViewHolder(binding.getRoot());
- }
-
- @Override
- public void onBindViewHolder(@NonNull ViewHolder viewHolder, int position) {
- viewHolder.bind(position);
- }
-
- @Override
- public int getItemCount() {
- return uploadableFiles.size();
- }
-
- public class ViewHolder extends RecyclerView.ViewHolder {
-
-
- RelativeLayout rlContainer;
- SimpleDraweeView background;
- ImageView ivError;
-
- ImageView ivCross;
-
- public ViewHolder(@NonNull View itemView) {
- super(itemView);
- rlContainer = binding.rlContainer;
- background = binding.ivThumbnail;
- ivError = binding.ivError;
- ivCross = binding.icCross;
- }
-
- /**
- * Binds a row item to the ViewHolder
- * @param position
- */
- public void bind(int position) {
- UploadableFile uploadableFile = uploadableFiles.get(position);
- Uri uri = uploadableFile.getMediaUri();
- background.setImageURI(Uri.fromFile(new File(String.valueOf(uri))));
- if (position == callback.getCurrentSelectedFilePosition()) {
- GradientDrawable border = new GradientDrawable();
- border.setShape(GradientDrawable.RECTANGLE);
- border.setStroke(8, context.getResources().getColor(R.color.primaryColor));
- rlContainer.setEnabled(true);
- rlContainer.setClickable(true);
- rlContainer.setAlpha(1.0f);
- rlContainer.setBackground(border);
- if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
- rlContainer.setElevation(10);
- }
- } else {
- rlContainer.setEnabled(false);
- rlContainer.setClickable(false);
- rlContainer.setAlpha(0.7f);
- rlContainer.setBackground(null);
- if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
- rlContainer.setElevation(0);
- }
- }
-
- ivCross.setOnClickListener(v -> {
- if(listener != null) {
- listener.onThumbnailDeleted(position);
- }
- });
- }
- }
-
- /**
- * Callback used to get the current selected file position
- */
- interface Callback {
-
- int getCurrentSelectedFilePosition();
- }
-
- /**
- * Interface to listen to thumbnail delete events
- */
-
- public interface OnThumbnailDeletedListener {
- void onThumbnailDeleted(int position);
- }
-
-}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/ThumbnailsAdapter.kt b/app/src/main/java/fr/free/nrw/commons/upload/ThumbnailsAdapter.kt
new file mode 100644
index 0000000000..d467f9bf63
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/upload/ThumbnailsAdapter.kt
@@ -0,0 +1,90 @@
+package fr.free.nrw.commons.upload
+
+import android.graphics.drawable.GradientDrawable
+import android.net.Uri
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import android.widget.ImageView
+import android.widget.RelativeLayout
+import androidx.core.content.ContextCompat
+import androidx.recyclerview.widget.RecyclerView
+import com.facebook.drawee.view.SimpleDraweeView
+import fr.free.nrw.commons.R
+import fr.free.nrw.commons.databinding.ItemUploadThumbnailBinding
+import fr.free.nrw.commons.filepicker.UploadableFile
+import java.io.File
+
+/**
+ * The adapter class for image thumbnails to be shown while uploading.
+ */
+internal class ThumbnailsAdapter(private val callback: Callback) :
+ RecyclerView.Adapter() {
+
+ var onThumbnailDeletedListener: OnThumbnailDeletedListener? = null
+ var uploadableFiles: List = emptyList()
+ set(value) {
+ field = value
+ notifyDataSetChanged()
+ }
+
+ override fun onCreateViewHolder(viewGroup: ViewGroup, i: Int) = ViewHolder(
+ ItemUploadThumbnailBinding.inflate(
+ LayoutInflater.from(viewGroup.context), viewGroup, false
+ )
+ )
+
+ override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) = viewHolder.bind(position)
+
+ override fun getItemCount(): Int = uploadableFiles.size
+
+ inner class ViewHolder(val binding: ItemUploadThumbnailBinding) :
+ RecyclerView.ViewHolder(binding.root) {
+ private val rlContainer: RelativeLayout = binding.rlContainer
+ private val background: SimpleDraweeView = binding.ivThumbnail
+ private val ivError: ImageView = binding.ivError
+ private val ivCross: ImageView = binding.icCross
+
+ /**
+ * Binds a row item to the ViewHolder
+ */
+ fun bind(position: Int) {
+ val uploadableFile = uploadableFiles[position]
+ val uri = uploadableFile.getMediaUri()
+ background.setImageURI(Uri.fromFile(File(uri.toString())))
+ if (position == callback.getCurrentSelectedFilePosition()) {
+ val border = GradientDrawable()
+ border.shape = GradientDrawable.RECTANGLE
+ border.setStroke(8, ContextCompat.getColor(itemView.context, R.color.primaryColor))
+ rlContainer.isEnabled = true
+ rlContainer.isClickable = true
+ rlContainer.alpha = 1.0f
+ rlContainer.background = border
+ rlContainer.elevation = 10f
+ } else {
+ rlContainer.isEnabled = false
+ rlContainer.isClickable = false
+ rlContainer.alpha = 0.7f
+ rlContainer.background = null
+ rlContainer.elevation = 0f
+ }
+
+ ivCross.setOnClickListener {
+ onThumbnailDeletedListener?.onThumbnailDeleted(position)
+ }
+ }
+ }
+
+ /**
+ * Callback used to get the current selected file position
+ */
+ internal fun interface Callback {
+ fun getCurrentSelectedFilePosition(): Int
+ }
+
+ /**
+ * Interface to listen to thumbnail delete events
+ */
+ fun interface OnThumbnailDeletedListener {
+ fun onThumbnailDeleted(position: Int)
+ }
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.java
index f487731807..315121692f 100644
--- a/app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.java
+++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.java
@@ -448,7 +448,6 @@ public static void setUploadIsOfAPlace(final boolean uploadOfAPlace) {
}
private void receiveSharedItems() {
- ThumbnailsAdapter.context=this;
final Intent intent = getIntent();
final String action = intent.getAction();
if (Intent.ACTION_SEND.equals(action) || Intent.ACTION_SEND_MULTIPLE.equals(action)) {
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadBaseFragment.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadBaseFragment.java
deleted file mode 100644
index 0219c10cf6..0000000000
--- a/app/src/main/java/fr/free/nrw/commons/upload/UploadBaseFragment.java
+++ /dev/null
@@ -1,41 +0,0 @@
-package fr.free.nrw.commons.upload;
-
-import android.os.Bundle;
-import androidx.annotation.Nullable;
-import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
-
-/**
- * The base fragment of the fragments in upload
- */
-public class UploadBaseFragment extends CommonsDaggerSupportFragment {
-
- public Callback callback;
- public static final String CALLBACK = "callback";
-
- @Override
- public void onCreate(@Nullable Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- }
-
- public void setCallback(Callback callback) {
- this.callback = callback;
- }
-
- protected void onBecameVisible() {
- }
-
- public interface Callback {
-
- void onNextButtonClicked(int index);
-
- void onPreviousButtonClicked(int index);
-
- void showProgress(boolean shouldShow);
-
- int getIndexInViewFlipper(UploadBaseFragment fragment);
-
- int getTotalNumberOfSteps();
-
- boolean isWLMUpload();
- }
-}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadBaseFragment.kt b/app/src/main/java/fr/free/nrw/commons/upload/UploadBaseFragment.kt
new file mode 100644
index 0000000000..0cb47273e9
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadBaseFragment.kt
@@ -0,0 +1,26 @@
+package fr.free.nrw.commons.upload
+
+import fr.free.nrw.commons.di.CommonsDaggerSupportFragment
+
+/**
+ * The base fragment of the fragments in upload
+ */
+abstract class UploadBaseFragment : CommonsDaggerSupportFragment() {
+ lateinit var callback: Callback
+
+ protected open fun onBecameVisible() = Unit
+
+ interface Callback {
+ val totalNumberOfSteps: Int
+ val isWLMUpload: Boolean
+
+ fun onNextButtonClicked(index: Int)
+ fun onPreviousButtonClicked(index: Int)
+ fun showProgress(shouldShow: Boolean)
+ fun getIndexInViewFlipper(fragment: UploadBaseFragment?): Int
+ }
+
+ companion object {
+ const val CALLBACK: String = "callback"
+ }
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadController.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadController.java
deleted file mode 100644
index d4f8ad62ca..0000000000
--- a/app/src/main/java/fr/free/nrw/commons/upload/UploadController.java
+++ /dev/null
@@ -1,163 +0,0 @@
-package fr.free.nrw.commons.upload;
-
-import android.accounts.Account;
-import android.annotation.SuppressLint;
-import android.content.ContentResolver;
-import android.content.Context;
-import android.content.res.AssetFileDescriptor;
-import android.database.Cursor;
-import android.net.Uri;
-import android.provider.MediaStore;
-import android.text.TextUtils;
-import fr.free.nrw.commons.Media;
-import fr.free.nrw.commons.R;
-import fr.free.nrw.commons.auth.SessionManager;
-import fr.free.nrw.commons.contributions.Contribution;
-import fr.free.nrw.commons.kvstore.JsonKvStore;
-import fr.free.nrw.commons.settings.Prefs;
-import fr.free.nrw.commons.utils.ViewUtil;
-import java.io.BufferedInputStream;
-import java.io.File;
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.Date;
-import javax.inject.Inject;
-import javax.inject.Singleton;
-import timber.log.Timber;
-
-@Singleton
-public class UploadController {
-
- private final SessionManager sessionManager;
- private final Context context;
- private final JsonKvStore store;
-
- @Inject
- public UploadController(final SessionManager sessionManager,
- final Context context,
- final JsonKvStore store) {
- this.sessionManager = sessionManager;
- this.context = context;
- this.store = store;
- }
-
- /**
- * Starts a new upload task.
- *
- * @param contribution the contribution object
- */
- @SuppressLint("StaticFieldLeak")
- public void prepareMedia(final Contribution contribution) {
- //Set creator, desc, and license
-
- // If author name is enabled and set, use it
- final Media media = contribution.getMedia();
- if (store.getBoolean("useAuthorName", false)) {
- final String authorName = store.getString("authorName", "");
- media.setAuthor(authorName);
- }
-
- if (TextUtils.isEmpty(media.getAuthor())) {
- final Account currentAccount = sessionManager.getCurrentAccount();
- if (currentAccount == null) {
- Timber.d("Current account is null");
- ViewUtil.showLongToast(context, context.getString(R.string.user_not_logged_in));
- sessionManager.forceLogin(context);
- return;
- }
- media.setAuthor(sessionManager.getUserName());
- }
-
- if (media.getFallbackDescription() == null) {
- media.setFallbackDescription("");
- }
-
- final String license = store.getString(Prefs.DEFAULT_LICENSE, Prefs.Licenses.CC_BY_SA_3);
- media.setLicense(license);
-
- buildUpload(contribution);
- }
-
- /**
- * Make the Contribution object ready to be uploaded
- * @param contribution
- * @return
- */
- private void buildUpload(final Contribution contribution) {
- final ContentResolver contentResolver = context.getContentResolver();
-
- contribution.setDataLength(resolveDataLength(contentResolver, contribution));
-
- final String mimeType = resolveMimeType(contentResolver, contribution);
-
- if (mimeType != null) {
- Timber.d("MimeType is: %s", mimeType);
- contribution.setMimeType(mimeType);
- if(mimeType.startsWith("image/") && contribution.getDateCreated() == null){
- contribution.setDateCreated(resolveDateTakenOrNow(contentResolver, contribution));
- }
- }
- }
-
- private String resolveMimeType(final ContentResolver contentResolver, final Contribution contribution) {
- final String mimeType = contribution.getMimeType();
- if (mimeType == null || TextUtils.isEmpty(mimeType) || mimeType.endsWith("*")) {
- return contentResolver.getType(contribution.getLocalUri());
- }
- return mimeType;
- }
-
- private long resolveDataLength(final ContentResolver contentResolver, final Contribution contribution) {
- try {
- if (contribution.getDataLength() <= 0) {
- Timber.d("UploadController/doInBackground, contribution.getLocalUri():%s", contribution.getLocalUri());
- final AssetFileDescriptor assetFileDescriptor = contentResolver
- .openAssetFileDescriptor(Uri.fromFile(new File(contribution.getLocalUri().getPath())), "r");
- if (assetFileDescriptor != null) {
- final long length = assetFileDescriptor.getLength();
- return length != -1 ? length
- : countBytes(contentResolver.openInputStream(contribution.getLocalUri()));
- }
- }
- } catch (final IOException | NullPointerException | SecurityException e) {
- Timber.e(e, "Exception occurred while uploading image");
- }
- return contribution.getDataLength();
- }
-
- private Date resolveDateTakenOrNow(final ContentResolver contentResolver, final Contribution contribution) {
- Timber.d("local uri %s", contribution.getLocalUri());
- try(final Cursor cursor = dateTakenCursor(contentResolver, contribution)) {
- if (cursor != null && cursor.getCount() != 0 && cursor.getColumnCount() != 0) {
- cursor.moveToFirst();
- final Date dateCreated = new Date(cursor.getLong(0));
- if (dateCreated.after(new Date(0))) {
- return dateCreated;
- }
- }
- return new Date();
- }
- }
-
- private Cursor dateTakenCursor(final ContentResolver contentResolver, final Contribution contribution) {
- return contentResolver.query(contribution.getLocalUri(),
- new String[]{MediaStore.Images.ImageColumns.DATE_TAKEN}, null, null, null);
- }
-
-
- /**
- * Counts the number of bytes in {@code stream}.
- *
- * @param stream the stream
- * @return the number of bytes in {@code stream}
- * @throws IOException if an I/O error occurs
- */
- private long countBytes(final InputStream stream) throws IOException {
- long count = 0;
- final BufferedInputStream bis = new BufferedInputStream(stream);
- while (bis.read() != -1) {
- count++;
- }
- return count;
- }
-}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadController.kt b/app/src/main/java/fr/free/nrw/commons/upload/UploadController.kt
new file mode 100644
index 0000000000..706e8ba2ba
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadController.kt
@@ -0,0 +1,166 @@
+package fr.free.nrw.commons.upload
+
+import android.annotation.SuppressLint
+import android.content.ContentResolver
+import android.content.Context
+import android.database.Cursor
+import android.net.Uri
+import android.provider.MediaStore
+import android.text.TextUtils
+import fr.free.nrw.commons.R
+import fr.free.nrw.commons.auth.SessionManager
+import fr.free.nrw.commons.contributions.Contribution
+import fr.free.nrw.commons.kvstore.JsonKvStore
+import fr.free.nrw.commons.settings.Prefs
+import fr.free.nrw.commons.utils.ViewUtil.showLongToast
+import timber.log.Timber
+import java.io.BufferedInputStream
+import java.io.File
+import java.io.IOException
+import java.io.InputStream
+import java.util.Date
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class UploadController @Inject constructor(
+ private val sessionManager: SessionManager,
+ private val context: Context,
+ private val store: JsonKvStore
+) {
+ /**
+ * Starts a new upload task.
+ *
+ * @param contribution the contribution object
+ */
+ @SuppressLint("StaticFieldLeak")
+ fun prepareMedia(contribution: Contribution) {
+ //Set creator, desc, and license
+
+ // If author name is enabled and set, use it
+
+ val media = contribution.media
+ if (store.getBoolean("useAuthorName", false)) {
+ val authorName = store.getString("authorName", "")
+ media.author = authorName
+ }
+
+ if (media.author.isNullOrEmpty()) {
+ val currentAccount = sessionManager.currentAccount
+ if (currentAccount == null) {
+ Timber.d("Current account is null")
+ showLongToast(context, context.getString(R.string.user_not_logged_in))
+ sessionManager.forceLogin(context)
+ return
+ }
+ media.author = sessionManager.userName
+ }
+
+ if (media.fallbackDescription == null) {
+ media.fallbackDescription = ""
+ }
+
+ val license = store.getString(Prefs.DEFAULT_LICENSE, Prefs.Licenses.CC_BY_SA_3)
+ media.license = license
+
+ buildUpload(contribution)
+ }
+
+ private fun buildUpload(contribution: Contribution) {
+ val contentResolver = context.contentResolver
+
+ contribution.dataLength = resolveDataLength(contentResolver, contribution)
+
+ val mimeType = resolveMimeType(contentResolver, contribution)
+
+ if (mimeType != null) {
+ Timber.d("MimeType is: %s", mimeType)
+ contribution.mimeType = mimeType
+ if (mimeType.startsWith("image/") && contribution.dateCreated == null) {
+ contribution.dateCreated = resolveDateTakenOrNow(contentResolver, contribution)
+ }
+ }
+ }
+
+ private fun resolveMimeType(
+ contentResolver: ContentResolver,
+ contribution: Contribution
+ ): String? {
+ val mimeType: String? = contribution.mimeType
+ return if (mimeType.isNullOrEmpty() || mimeType.endsWith("*")) {
+ contentResolver.getType(contribution.localUri!!)
+ } else {
+ mimeType
+ }
+ }
+
+ private fun resolveDataLength(
+ contentResolver: ContentResolver,
+ contribution: Contribution
+ ): Long {
+ try {
+ if (contribution.dataLength <= 0) {
+ Timber.d(
+ "UploadController/doInBackground, contribution.getLocalUri():%s",
+ contribution.localUri
+ )
+
+ contentResolver.openAssetFileDescriptor(
+ Uri.fromFile(File(contribution.localUri!!.path!!)), "r"
+ )?.use {
+ return if (it.length != -1L) it.length
+ else countBytes(contentResolver.openInputStream(contribution.localUri))
+ }
+ }
+ } catch (e: IOException) {
+ Timber.e(e, "Exception occurred while uploading image")
+ } catch (e: NullPointerException) {
+ Timber.e(e, "Exception occurred while uploading image")
+ } catch (e: SecurityException) {
+ Timber.e(e, "Exception occurred while uploading image")
+ }
+ return contribution.dataLength
+ }
+
+ private fun resolveDateTakenOrNow(
+ contentResolver: ContentResolver,
+ contribution: Contribution
+ ): Date {
+ Timber.d("local uri %s", contribution.localUri)
+ dateTakenCursor(contentResolver, contribution).use { cursor ->
+ if (cursor != null && cursor.count != 0 && cursor.columnCount != 0) {
+ cursor.moveToFirst()
+ val dateCreated = Date(cursor.getLong(0))
+ if (dateCreated.after(Date(0))) {
+ return dateCreated
+ }
+ }
+ return Date()
+ }
+ }
+
+ private fun dateTakenCursor(
+ contentResolver: ContentResolver,
+ contribution: Contribution
+ ): Cursor? = contentResolver.query(
+ contribution.localUri!!,
+ arrayOf(MediaStore.Images.ImageColumns.DATE_TAKEN), null, null, null
+ )
+
+ /**
+ * Counts the number of bytes in `stream`.
+ *
+ * @param stream the stream
+ * @return the number of bytes in `stream`
+ * @throws IOException if an I/O error occurs
+ */
+ @Throws(IOException::class)
+ private fun countBytes(stream: InputStream?): Long {
+ var count: Long = 0
+ val bis = BufferedInputStream(stream)
+ while (bis.read() != -1) {
+ count++
+ }
+ return count
+ }
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadItem.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadItem.java
deleted file mode 100644
index b3c16b9629..0000000000
--- a/app/src/main/java/fr/free/nrw/commons/upload/UploadItem.java
+++ /dev/null
@@ -1,175 +0,0 @@
-package fr.free.nrw.commons.upload;
-
-import android.annotation.SuppressLint;
-import android.net.Uri;
-import androidx.annotation.Nullable;
-import fr.free.nrw.commons.Utils;
-import fr.free.nrw.commons.filepicker.MimeTypeMapWrapper;
-import fr.free.nrw.commons.nearby.Place;
-import fr.free.nrw.commons.utils.ImageUtils;
-import io.reactivex.subjects.BehaviorSubject;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-public class UploadItem {
-
- private Uri mediaUri;
- private final String mimeType;
- private ImageCoordinates gpsCoords;
- private List uploadMediaDetails;
- private Place place;
- private final long createdTimestamp;
- private final String createdTimestampSource;
- private final BehaviorSubject imageQuality;
- private boolean hasInvalidLocation;
- private boolean isWLMUpload = false;
- private String countryCode;
- private String fileCreatedDateString; //according to EXIF data
-
- /**
- * Uri of uploadItem
- * Uri points to image location or name, eg content://media/external/images/camera/10495 (Android 10)
- */
- private Uri contentUri;
-
-
- @SuppressLint("CheckResult")
- UploadItem(final Uri mediaUri,
- final String mimeType,
- final ImageCoordinates gpsCoords,
- final Place place,
- final long createdTimestamp,
- final String createdTimestampSource,
- final Uri contentUri,
- final String fileCreatedDateString) {
- this.createdTimestampSource = createdTimestampSource;
- uploadMediaDetails = new ArrayList<>(Collections.singletonList(new UploadMediaDetail()));
- this.place = place;
- this.mediaUri = mediaUri;
- this.mimeType = mimeType;
- this.gpsCoords = gpsCoords;
- this.createdTimestamp = createdTimestamp;
- this.contentUri = contentUri;
- imageQuality = BehaviorSubject.createDefault(ImageUtils.IMAGE_WAIT);
- this.fileCreatedDateString = fileCreatedDateString;
- }
-
- public String getCreatedTimestampSource() {
- return createdTimestampSource;
- }
-
- public ImageCoordinates getGpsCoords() {
- return gpsCoords;
- }
-
- public List getUploadMediaDetails() {
- return uploadMediaDetails;
- }
-
- public long getCreatedTimestamp() {
- return createdTimestamp;
- }
-
- public Uri getMediaUri() {
- return mediaUri;
- }
-
- public int getImageQuality() {
- return imageQuality.getValue();
- }
-
- /**
- * getContentUri.
- * @return Uri of uploadItem
- * Uri points to image location or name, eg content://media/external/images/camera/10495 (Android 10)
- */
- public Uri getContentUri() { return contentUri; }
-
- public String getFileCreatedDateString() { return fileCreatedDateString; }
-
- public void setImageQuality(final int imageQuality) {
- this.imageQuality.onNext(imageQuality);
- }
-
- /**
- * Sets the corresponding place to the uploadItem
- *
- * @param place geolocated Wikidata item
- */
- public void setPlace(Place place) {
- this.place = place;
- }
-
- public Place getPlace() {
- return place;
- }
-
- public void setMediaDetails(final List uploadMediaDetails) {
- this.uploadMediaDetails = uploadMediaDetails;
- }
-
- public void setWLMUpload(final boolean WLMUpload) {
- isWLMUpload = WLMUpload;
- }
-
- public boolean isWLMUpload() {
- return isWLMUpload;
- }
-
- @Override
- public boolean equals(@Nullable final Object obj) {
- if (!(obj instanceof UploadItem)) {
- return false;
- }
- return mediaUri.toString().contains(((UploadItem) (obj)).mediaUri.toString());
-
- }
-
- @Override
- public int hashCode() {
- return mediaUri.hashCode();
- }
-
- /**
- * Choose a filename for the media. Currently, the caption is used as a filename. If several
- * languages have been entered, the first language is used.
- */
- public String getFileName() {
- return Utils.fixExtension(uploadMediaDetails.get(0).getCaptionText(),
- MimeTypeMapWrapper.getExtensionFromMimeType(mimeType));
- }
-
- public void setGpsCoords(final ImageCoordinates gpsCoords) {
- this.gpsCoords = gpsCoords;
- }
-
- public void setHasInvalidLocation(boolean hasInvalidLocation) {
- this.hasInvalidLocation = hasInvalidLocation;
- }
-
- public boolean hasInvalidLocation() {
- return hasInvalidLocation;
- }
-
- public void setCountryCode(final String countryCode) {
- this.countryCode = countryCode;
- }
-
- @Nullable
- public String getCountryCode() {
- return countryCode;
- }
-
- /**
- * Sets both the contentUri and mediaUri to the specified Uri.
- * This method allows you to assign the same Uri to both the contentUri and mediaUri
- * properties.
- *
- * @param uri The Uri to be set as both the contentUri and mediaUri.
- */
- public void setContentUri(Uri uri) {
- contentUri = uri;
- mediaUri = uri;
- }
-}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadItem.kt b/app/src/main/java/fr/free/nrw/commons/upload/UploadItem.kt
new file mode 100644
index 0000000000..370ef960ac
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadItem.kt
@@ -0,0 +1,65 @@
+package fr.free.nrw.commons.upload
+
+import android.net.Uri
+import fr.free.nrw.commons.Utils
+import fr.free.nrw.commons.filepicker.MimeTypeMapWrapper.Companion.getExtensionFromMimeType
+import fr.free.nrw.commons.nearby.Place
+import fr.free.nrw.commons.utils.ImageUtils
+import io.reactivex.subjects.BehaviorSubject
+
+class UploadItem(
+ var mediaUri: Uri?,
+ val mimeType: String?,
+ var gpsCoords: ImageCoordinates?,
+ var place: Place?,
+ val createdTimestamp: Long?,
+ val createdTimestampSource: String?,
+ /**
+ * Uri of uploadItem
+ * Uri points to image location or name, eg content://media/external/images/camera/10495 (Android 10)
+ */
+ var contentUri: Uri?,
+ //according to EXIF data
+ val fileCreatedDateString: String?
+) {
+ var imageQuality: Int = ImageUtils.IMAGE_WAIT
+ var uploadMediaDetails: MutableList = mutableListOf(UploadMediaDetail())
+ var hasInvalidLocation = false
+ var isWLMUpload = false
+ var countryCode: String? = null
+
+ /**
+ * Choose a filename for the media. Currently, the caption is used as a filename. If several
+ * languages have been entered, the first language is used.
+ */
+ val filename: String
+ get() = Utils.fixExtension(
+ uploadMediaDetails[0].captionText,
+ getExtensionFromMimeType(mimeType)
+ )
+
+ fun hasInvalidLocation(): Boolean = hasInvalidLocation
+
+ /**
+ * Sets both the contentUri and mediaUri to the specified Uri.
+ * This method allows you to assign the same Uri to both the contentUri and mediaUri
+ * properties.
+ *
+ * @param uri The Uri to be set as both the contentUri and mediaUri.
+ */
+ fun setContentAndMediaUri(uri: Uri) {
+ contentUri = uri
+ mediaUri = uri
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (other !is UploadItem) {
+ return false
+ }
+ return mediaUri.toString().contains((other).mediaUri.toString())
+ }
+
+ override fun hashCode(): Int {
+ return mediaUri.hashCode()
+ }
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadMediaDetailInputFilter.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadMediaDetailInputFilter.java
deleted file mode 100644
index f2c444ee7a..0000000000
--- a/app/src/main/java/fr/free/nrw/commons/upload/UploadMediaDetailInputFilter.java
+++ /dev/null
@@ -1,81 +0,0 @@
-package fr.free.nrw.commons.upload;
-
-import android.text.InputFilter;
-import android.text.Spanned;
-import java.util.regex.Pattern;
-
-/**
- * An {@link InputFilter} class that removes characters blocklisted in Wikimedia titles. The list
- * of blocklisted characters is linked below.
- * @see wikimedia.org
- */
-public class UploadMediaDetailInputFilter implements InputFilter {
- private final Pattern[] patterns;
-
- /**
- * Initializes the blocklisted patterns.
- */
- public UploadMediaDetailInputFilter() {
- patterns = new Pattern[]{
- Pattern.compile("[\\x{00A0}\\x{1680}\\x{180E}\\x{2000}-\\x{200B}\\x{2028}\\x{2029}\\x{202F}\\x{205F}]"),
- Pattern.compile("[\\x{202A}-\\x{202E}]"),
- Pattern.compile("\\p{Cc}"),
- Pattern.compile("\\x{3A}"), // Added for colon(:)
- Pattern.compile("\\x{FEFF}"),
- Pattern.compile("\\x{00AD}"),
- Pattern.compile("[\\x{E000}-\\x{F8FF}\\x{FFF0}-\\x{FFFF}]"),
- Pattern.compile("[^\\x{0000}-\\x{FFFF}\\p{sc=Han}]")
- };
- }
-
- /**
- * Checks if the source text contains any blocklisted characters.
- * @param source input text
- * @return contains a blocklisted character
- */
- private Boolean checkBlocklisted(final CharSequence source) {
- for (final Pattern pattern: patterns) {
- if (pattern.matcher(source).find()) {
- return true;
- }
- }
-
- return false;
- }
-
- /**
- * Removes any blocklisted characters from the source text.
- * @param source input text
- * @return a cleaned character sequence
- */
- private CharSequence removeBlocklisted(CharSequence source) {
- for (final Pattern pattern: patterns) {
- source = pattern.matcher(source).replaceAll("");
- }
-
- return source;
- }
-
- /**
- * Filters out any blocklisted characters.
- * @param source {@inheritDoc}
- * @param start {@inheritDoc}
- * @param end {@inheritDoc}
- * @param dest {@inheritDoc}
- * @param dstart {@inheritDoc}
- * @param dend {@inheritDoc}
- * @return {@inheritDoc}
- */
- @Override
- public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart,
- int dend) {
- if (checkBlocklisted(source)) {
- if (start == dstart && dest.length() > 0) {
- return dest;
- }
-
- return removeBlocklisted(source);
- }
- return null;
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadMediaDetailInputFilter.kt b/app/src/main/java/fr/free/nrw/commons/upload/UploadMediaDetailInputFilter.kt
new file mode 100644
index 0000000000..d4baf21c88
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadMediaDetailInputFilter.kt
@@ -0,0 +1,69 @@
+package fr.free.nrw.commons.upload
+
+import android.text.InputFilter
+import android.text.Spanned
+import java.util.regex.Pattern
+
+/**
+ * An [InputFilter] class that removes characters blocklisted in Wikimedia titles. The list
+ * of blocklisted characters is linked below.
+ * @see [](https://commons.wikimedia.org/wiki/MediaWiki:Titleblacklist)wikimedia.org
+ */
+class UploadMediaDetailInputFilter : InputFilter {
+ private val patterns = listOf(
+ Pattern.compile("[\\x{00A0}\\x{1680}\\x{180E}\\x{2000}-\\x{200B}\\x{2028}\\x{2029}\\x{202F}\\x{205F}]"),
+ Pattern.compile("[\\x{202A}-\\x{202E}]"),
+ Pattern.compile("\\p{Cc}"),
+ Pattern.compile("\\x{3A}"), // Added for colon(:)
+ Pattern.compile("\\x{FEFF}"),
+ Pattern.compile("\\x{00AD}"),
+ Pattern.compile("[\\x{E000}-\\x{F8FF}\\x{FFF0}-\\x{FFFF}]"),
+ Pattern.compile("[^\\x{0000}-\\x{FFFF}\\p{sc=Han}]")
+ )
+
+ /**
+ * Checks if the source text contains any blocklisted characters.
+ * @param source input text
+ * @return contains a blocklisted character
+ */
+ private fun checkBlocklisted(source: CharSequence): Boolean =
+ patterns.any { it.matcher(source).find() }
+
+ /**
+ * Removes any blocklisted characters from the source text.
+ * @param source input text
+ * @return a cleaned character sequence
+ */
+ private fun removeBlocklisted(input: CharSequence): CharSequence {
+ var source = input
+ patterns.forEach {
+ source = it.matcher(source).replaceAll("")
+ }
+
+ return source
+ }
+
+ /**
+ * Filters out any blocklisted characters.
+ * @param source {@inheritDoc}
+ * @param start {@inheritDoc}
+ * @param end {@inheritDoc}
+ * @param dest {@inheritDoc}
+ * @param dstart {@inheritDoc}
+ * @param dend {@inheritDoc}
+ * @return {@inheritDoc}
+ */
+ override fun filter(
+ source: CharSequence, start: Int, end: Int, dest: Spanned, dstart: Int,
+ dend: Int
+ ): CharSequence? {
+ if (checkBlocklisted(source)) {
+ if (start == dstart && dest.isNotEmpty()) {
+ return dest
+ }
+
+ return removeBlocklisted(source)
+ }
+ return null
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadPresenter.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadPresenter.java
deleted file mode 100644
index 093412c250..0000000000
--- a/app/src/main/java/fr/free/nrw/commons/upload/UploadPresenter.java
+++ /dev/null
@@ -1,206 +0,0 @@
-package fr.free.nrw.commons.upload;
-
-import android.annotation.SuppressLint;
-import fr.free.nrw.commons.CommonsApplication;
-import fr.free.nrw.commons.R;
-import fr.free.nrw.commons.contributions.Contribution;
-import fr.free.nrw.commons.filepicker.UploadableFile;
-import fr.free.nrw.commons.kvstore.JsonKvStore;
-import fr.free.nrw.commons.repository.UploadRepository;
-import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailsContract;
-import io.reactivex.Observer;
-import io.reactivex.disposables.CompositeDisposable;
-import io.reactivex.disposables.Disposable;
-import io.reactivex.schedulers.Schedulers;
-import java.lang.reflect.Proxy;
-import java.util.List;
-import javax.inject.Inject;
-import javax.inject.Named;
-import javax.inject.Singleton;
-import timber.log.Timber;
-
-/**
- * The MVP pattern presenter of Upload GUI
- */
-@Singleton
-public class UploadPresenter implements UploadContract.UserActionListener {
-
- private static final UploadContract.View DUMMY = (UploadContract.View) Proxy.newProxyInstance(
- UploadContract.View.class.getClassLoader(),
- new Class[]{UploadContract.View.class}, (proxy, method, methodArgs) -> null);
- private final UploadRepository repository;
- private final JsonKvStore defaultKvStore;
- private UploadContract.View view = DUMMY;
- @Inject
- UploadMediaDetailsContract.UserActionListener presenter;
-
- private CompositeDisposable compositeDisposable;
- public static final String COUNTER_OF_CONSECUTIVE_UPLOADS_WITHOUT_COORDINATES
- = "number_of_consecutive_uploads_without_coordinates";
-
- public static final int CONSECUTIVE_UPLOADS_WITHOUT_COORDINATES_REMINDER_THRESHOLD = 10;
-
-
- @Inject
- UploadPresenter(UploadRepository uploadRepository,
- @Named("default_preferences") JsonKvStore defaultKvStore) {
- this.repository = uploadRepository;
- this.defaultKvStore = defaultKvStore;
- compositeDisposable = new CompositeDisposable();
- }
-
-
- /**
- * Called by the submit button in {@link UploadActivity}
- */
- @SuppressLint("CheckResult")
- @Override
- public void handleSubmit() {
- boolean hasLocationProvidedForNewUploads = false;
- for (UploadItem item : repository.getUploads()) {
- if (item.getGpsCoords().getImageCoordsExists()) {
- hasLocationProvidedForNewUploads = true;
- }
- }
- boolean hasManyConsecutiveUploadsWithoutLocation = defaultKvStore.getInt(
- COUNTER_OF_CONSECUTIVE_UPLOADS_WITHOUT_COORDINATES, 0) >=
- CONSECUTIVE_UPLOADS_WITHOUT_COORDINATES_REMINDER_THRESHOLD;
-
- if (hasManyConsecutiveUploadsWithoutLocation && !hasLocationProvidedForNewUploads) {
- defaultKvStore.putInt(COUNTER_OF_CONSECUTIVE_UPLOADS_WITHOUT_COORDINATES, 0);
- view.showAlertDialog(
- R.string.location_message,
- () -> {defaultKvStore.putInt(
- COUNTER_OF_CONSECUTIVE_UPLOADS_WITHOUT_COORDINATES,
- 0);
- processContributionsForSubmission();
- });
- } else {
- processContributionsForSubmission();
- }
- }
-
- private void processContributionsForSubmission() {
- if (view.isLoggedIn()) {
- view.showProgress(true);
- repository.buildContributions()
- .observeOn(Schedulers.io())
- .subscribe(new Observer() {
- @Override
- public void onSubscribe(Disposable d) {
- view.showProgress(false);
- if (defaultKvStore
- .getBoolean(CommonsApplication.IS_LIMITED_CONNECTION_MODE_ENABLED,
- false)) {
- view.showMessage(R.string.uploading_queued);
- } else {
- view.showMessage(R.string.uploading_started);
- }
-
- compositeDisposable.add(d);
- }
-
- @Override
- public void onNext(Contribution contribution) {
- if (contribution.getDecimalCoords() == null) {
- final int recentCount = defaultKvStore.getInt(
- COUNTER_OF_CONSECUTIVE_UPLOADS_WITHOUT_COORDINATES, 0);
- defaultKvStore.putInt(
- COUNTER_OF_CONSECUTIVE_UPLOADS_WITHOUT_COORDINATES, recentCount + 1);
- } else {
- defaultKvStore.putInt(
- COUNTER_OF_CONSECUTIVE_UPLOADS_WITHOUT_COORDINATES, 0);
- }
- repository.prepareMedia(contribution);
- contribution.setState(Contribution.STATE_QUEUED);
- repository.saveContribution(contribution);
- }
-
- @Override
- public void onError(Throwable e) {
- view.showMessage(R.string.upload_failed);
- repository.cleanup();
- view.returnToMainActivity();
- compositeDisposable.clear();
- Timber.e("failed to upload: " + e.getMessage());
-
- //is submission error, not need to go to the uploadActivity
- //not start the uploading progress
- }
-
- @Override
- public void onComplete() {
- view.makeUploadRequest();
- repository.cleanup();
- view.returnToMainActivity();
- compositeDisposable.clear();
-
- //after finish the uploadActivity, if successful,
- //directly go to the upload progress activity
- view.goToUploadProgressActivity();
- }
- });
- } else {
- view.askUserToLogIn();
- }
- }
-
- /**
- * Calls checkImageQuality of UploadMediaPresenter to check image quality of next image
- *
- * @param uploadItemIndex Index of next image, whose quality is to be checked
- */
- @Override
- public void checkImageQuality(int uploadItemIndex) {
- UploadItem uploadItem = repository.getUploadItem(uploadItemIndex);
- presenter.checkImageQuality(uploadItem, uploadItemIndex);
- }
-
-
- @Override
- public void deletePictureAtIndex(int index) {
- List uploadableFiles = view.getUploadableFiles();
- if (index == uploadableFiles.size() - 1) {
- // If the next fragment to be shown is not one of the MediaDetailsFragment
- // lets hide the top card so that it doesn't appear on the other fragments
- view.showHideTopCard(false);
- }
- view.setImageCancelled(true);
- repository.deletePicture(uploadableFiles.get(index).getFilePath());
- if (uploadableFiles.size() == 1) {
- view.showMessage(R.string.upload_cancelled);
- view.finish();
- return;
- } else {
- if (presenter != null) {
- presenter.updateImageQualitiesJSON(uploadableFiles.size(), index);
- }
- 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
- UploadItem uploadItem = repository.getUploadItem(index);
- presenter.checkImageQuality(uploadItem, index);
- }
- }
- if (uploadableFiles.size() < 2) {
- view.showHideTopCard(false);
- }
-
- //In case lets update the number of uploadable media
- view.updateTopCardTitle();
-
- }
-
- @Override
- public void onAttachView(UploadContract.View view) {
- this.view = view;
- }
-
- @Override
- public void onDetachView() {
- this.view = DUMMY;
- compositeDisposable.clear();
- repository.cleanup();
- }
-
-}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadPresenter.kt b/app/src/main/java/fr/free/nrw/commons/upload/UploadPresenter.kt
new file mode 100644
index 0000000000..361ac1cee2
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadPresenter.kt
@@ -0,0 +1,192 @@
+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
+import fr.free.nrw.commons.kvstore.JsonKvStore
+import fr.free.nrw.commons.repository.UploadRepository
+import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailsContract
+import io.reactivex.Observer
+import io.reactivex.disposables.CompositeDisposable
+import io.reactivex.disposables.Disposable
+import io.reactivex.schedulers.Schedulers
+import timber.log.Timber
+import java.lang.reflect.Method
+import java.lang.reflect.Proxy
+import javax.inject.Inject
+import javax.inject.Named
+import javax.inject.Singleton
+
+/**
+ * The MVP pattern presenter of Upload GUI
+ */
+@Singleton
+class UploadPresenter @Inject internal constructor(
+ private val repository: UploadRepository,
+ @param:Named("default_preferences") private val defaultKvStore: JsonKvStore
+) : UploadContract.UserActionListener {
+ private var view = DUMMY
+
+ @Inject
+ lateinit var presenter: UploadMediaDetailsContract.UserActionListener
+
+ private val compositeDisposable = CompositeDisposable()
+
+ /**
+ * Called by the submit button in [UploadActivity]
+ */
+ @SuppressLint("CheckResult")
+ override fun handleSubmit() {
+ var hasLocationProvidedForNewUploads = false
+ for (item in repository.getUploads()) {
+ if (item.gpsCoords?.imageCoordsExists == true) {
+ hasLocationProvidedForNewUploads = true
+ }
+ }
+ val hasManyConsecutiveUploadsWithoutLocation = defaultKvStore.getInt(
+ COUNTER_OF_CONSECUTIVE_UPLOADS_WITHOUT_COORDINATES, 0
+ ) >=
+ CONSECUTIVE_UPLOADS_WITHOUT_COORDINATES_REMINDER_THRESHOLD
+
+ if (hasManyConsecutiveUploadsWithoutLocation && !hasLocationProvidedForNewUploads) {
+ defaultKvStore.putInt(COUNTER_OF_CONSECUTIVE_UPLOADS_WITHOUT_COORDINATES, 0)
+ view.showAlertDialog(
+ R.string.location_message
+ ) {
+ defaultKvStore.putInt(
+ COUNTER_OF_CONSECUTIVE_UPLOADS_WITHOUT_COORDINATES,
+ 0
+ )
+ processContributionsForSubmission()
+ }
+ } else {
+ processContributionsForSubmission()
+ }
+ }
+
+ private fun processContributionsForSubmission() {
+ if (view.isLoggedIn()) {
+ view.showProgress(true)
+ repository.buildContributions()
+ ?.observeOn(Schedulers.io())
+ ?.subscribe(object : Observer {
+ override fun onSubscribe(d: Disposable) {
+ view.showProgress(false)
+ if (defaultKvStore.getBoolean(IS_LIMITED_CONNECTION_MODE_ENABLED, false)) {
+ view.showMessage(R.string.uploading_queued)
+ } else {
+ view.showMessage(R.string.uploading_started)
+ }
+ compositeDisposable.add(d)
+ }
+
+ override fun onNext(contribution: Contribution) {
+ if (contribution.decimalCoords == null) {
+ val recentCount = defaultKvStore.getInt(
+ COUNTER_OF_CONSECUTIVE_UPLOADS_WITHOUT_COORDINATES, 0
+ )
+ defaultKvStore.putInt(
+ COUNTER_OF_CONSECUTIVE_UPLOADS_WITHOUT_COORDINATES, recentCount + 1
+ )
+ } else {
+ defaultKvStore.putInt(
+ COUNTER_OF_CONSECUTIVE_UPLOADS_WITHOUT_COORDINATES, 0
+ )
+ }
+ repository.prepareMedia(contribution)
+ contribution.state = Contribution.STATE_QUEUED
+ repository.saveContribution(contribution)
+ }
+
+ override fun onError(e: Throwable) {
+ view.showMessage(R.string.upload_failed)
+ repository.cleanup()
+ view.returnToMainActivity()
+ compositeDisposable.clear()
+ Timber.e(e, "failed to upload")
+
+ //is submission error, not need to go to the uploadActivity
+ //not start the uploading progress
+ }
+
+ override fun onComplete() {
+ view.makeUploadRequest()
+ repository.cleanup()
+ view.returnToMainActivity()
+ compositeDisposable.clear()
+
+ //after finish the uploadActivity, if successful,
+ //directly go to the upload progress activity
+ view.goToUploadProgressActivity()
+ }
+ })
+ } else {
+ view.askUserToLogIn()
+ }
+ }
+
+ /**
+ * Calls checkImageQuality of UploadMediaPresenter to check image quality of next image
+ *
+ * @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)
+ }
+
+ override fun deletePictureAtIndex(index: Int) {
+ val uploadableFiles = view.getUploadableFiles()
+ if (index == uploadableFiles!!.size - 1) {
+ // If the next fragment to be shown is not one of the MediaDetailsFragment
+ // lets hide the top card so that it doesn't appear on the other fragments
+ view.showHideTopCard(false)
+ }
+ view.setImageCancelled(true)
+ repository.deletePicture(uploadableFiles[index].getFilePath())
+ if (uploadableFiles.size == 1) {
+ view.showMessage(R.string.upload_cancelled)
+ view.finish()
+ return
+ }
+
+ presenter.updateImageQualitiesJSON(uploadableFiles.size, index)
+ 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)
+ }
+
+ if (uploadableFiles.size < 2) {
+ view.showHideTopCard(false)
+ }
+
+ //In case lets update the number of uploadable media
+ view.updateTopCardTitle()
+ }
+
+ override fun onAttachView(view: UploadContract.View) {
+ this.view = view
+ }
+
+ override fun onDetachView() {
+ view = DUMMY
+ compositeDisposable.clear()
+ repository.cleanup()
+ }
+
+ companion object {
+ private val DUMMY = Proxy.newProxyInstance(
+ UploadContract.View::class.java.classLoader,
+ arrayOf<*>>(UploadContract.View::class.java)
+ ) { _: Any?, _: Method?, _: Array? -> null } as UploadContract.View
+
+ const val COUNTER_OF_CONSECUTIVE_UPLOADS_WITHOUT_COORDINATES: String =
+ "number_of_consecutive_uploads_without_coordinates"
+
+ const val CONSECUTIVE_UPLOADS_WITHOUT_COORDINATES_REMINDER_THRESHOLD: Int = 10
+ }
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/categories/UploadCategoriesFragment.java b/app/src/main/java/fr/free/nrw/commons/upload/categories/UploadCategoriesFragment.java
deleted file mode 100644
index 1436ab7144..0000000000
--- a/app/src/main/java/fr/free/nrw/commons/upload/categories/UploadCategoriesFragment.java
+++ /dev/null
@@ -1,425 +0,0 @@
-package fr.free.nrw.commons.upload.categories;
-
-import static fr.free.nrw.commons.wikidata.WikidataConstants.SELECTED_NEARBY_PLACE_CATEGORY;
-
-import android.app.Activity;
-import android.app.ProgressDialog;
-import android.content.Context;
-import android.os.Bundle;
-import android.text.Editable;
-import android.view.KeyEvent;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.View.OnClickListener;
-import android.view.ViewGroup;
-import android.widget.Toast;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.appcompat.app.AppCompatActivity;
-import androidx.recyclerview.widget.LinearLayoutManager;
-import com.jakewharton.rxbinding2.view.RxView;
-import com.jakewharton.rxbinding2.widget.RxTextView;
-import fr.free.nrw.commons.CommonsApplication;
-import fr.free.nrw.commons.Media;
-import fr.free.nrw.commons.R;
-import fr.free.nrw.commons.auth.SessionManager;
-import fr.free.nrw.commons.category.CategoryItem;
-import fr.free.nrw.commons.contributions.ContributionsFragment;
-import fr.free.nrw.commons.databinding.UploadCategoriesFragmentBinding;
-import fr.free.nrw.commons.media.MediaDetailFragment;
-import fr.free.nrw.commons.upload.UploadActivity;
-import fr.free.nrw.commons.upload.UploadBaseFragment;
-import fr.free.nrw.commons.utils.DialogUtil;
-import io.reactivex.android.schedulers.AndroidSchedulers;
-import io.reactivex.disposables.Disposable;
-import java.util.List;
-import java.util.Objects;
-import java.util.concurrent.TimeUnit;
-import javax.inject.Inject;
-import kotlin.Unit;
-import timber.log.Timber;
-
-public class UploadCategoriesFragment extends UploadBaseFragment implements CategoriesContract.View {
-
- @Inject
- CategoriesContract.UserActionListener presenter;
- @Inject
- SessionManager sessionManager;
- private UploadCategoryAdapter adapter;
- private Disposable subscribe;
- /**
- * Current media
- */
- private Media media;
- /**
- * Progress Dialog for showing background process
- */
- private ProgressDialog progressDialog;
- /**
- * WikiText from the server
- */
- private String wikiText;
- private String nearbyPlaceCategory;
-
- private UploadCategoriesFragmentBinding binding;
-
- @Nullable
- @Override
- public View onCreateView(@NonNull final LayoutInflater inflater, @Nullable final ViewGroup container,
- @Nullable final Bundle savedInstanceState) {
- binding = UploadCategoriesFragmentBinding.inflate(inflater, container, false);
- return binding.getRoot();
- }
-
- @Override
- public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) {
- super.onViewCreated(view, savedInstanceState);
- final Bundle bundle = getArguments();
- if (bundle != null) {
- media = bundle.getParcelable("Existing_Categories");
- wikiText = bundle.getString("WikiText");
- nearbyPlaceCategory = bundle.getString(SELECTED_NEARBY_PLACE_CATEGORY);
- }
- init();
- presenter.getCategories().observe(getViewLifecycleOwner(), this::setCategories);
-
- }
-
- private void init() {
- if (binding == null) {
- return;
- }
- if (media == null) {
- if (callback != null) {
- binding.tvTitle.setText(getString(R.string.step_count, callback.getIndexInViewFlipper(this) + 1,
- callback.getTotalNumberOfSteps(), getString(R.string.categories_activity_title)));
- }
- } else {
- binding.tvTitle.setText(R.string.edit_categories);
- binding.tvSubtitle.setVisibility(View.GONE);
- binding.btnNext.setText(R.string.menu_save_categories);
- binding.btnPrevious.setText(R.string.menu_cancel_upload);
- }
-
- setTvSubTitle();
- binding.tooltip.setOnClickListener(new OnClickListener() {
- @Override
- public void onClick(final View v) {
- DialogUtil.showAlertDialog(requireActivity(),
- getString(R.string.categories_activity_title),
- getString(R.string.categories_tooltip),
- getString(android.R.string.ok),
- null);
- }
- });
- if (media == null) {
- presenter.onAttachView(this);
- } else {
- presenter.onAttachViewWithMedia(this, media);
- }
- binding.btnNext.setOnClickListener(v -> onNextButtonClicked());
- binding.btnPrevious.setOnClickListener(v -> onPreviousButtonClicked());
-
- initRecyclerView();
- addTextChangeListenerToEtSearch();
- }
-
- private void addTextChangeListenerToEtSearch() {
- if (binding == null) {
- return;
- }
- subscribe = RxTextView.textChanges(binding.etSearch)
- .doOnEach(v -> binding.tilContainerSearch.setError(null))
- .takeUntil(RxView.detaches(binding.etSearch))
- .debounce(500, TimeUnit.MILLISECONDS)
- .observeOn(AndroidSchedulers.mainThread())
- .subscribe(filter -> searchForCategory(filter.toString()), Timber::e);
- }
-
- /**
- * Removes the tv subtitle If the activity is the instance of [UploadActivity] and
- * if multiple files aren't selected.
- */
- private void setTvSubTitle() {
- final Activity activity = getActivity();
- if (activity instanceof UploadActivity) {
- final boolean isMultipleFileSelected = ((UploadActivity) activity).getIsMultipleFilesSelected();
- if (!isMultipleFileSelected) {
- binding.tvSubtitle.setVisibility(View.GONE);
- }
- }
- }
-
- private void searchForCategory(final String query) {
- presenter.searchForCategories(query);
- }
-
- private void initRecyclerView() {
- adapter = new UploadCategoryAdapter(categoryItem -> {
- presenter.onCategoryItemClicked(categoryItem);
- return Unit.INSTANCE;
- }, nearbyPlaceCategory);
-
- if (binding!=null) {
- binding.rvCategories.setLayoutManager(new LinearLayoutManager(getContext()));
- binding.rvCategories.setAdapter(adapter);
- }
- }
-
- @Override
- public void onDestroyView() {
- super.onDestroyView();
- presenter.onDetachView();
- subscribe.dispose();
- }
-
- @Override
- public void showProgress(final boolean shouldShow) {
- if (binding != null) {
- binding.pbCategories.setVisibility(shouldShow ? View.VISIBLE : View.GONE);
- }
- }
-
- @Override
- public void showError(final String error) {
- if (binding != null) {
- binding.tilContainerSearch.setError(error);
- }
- }
-
- @Override
- public void showError(final int stringResourceId) {
- if (binding != null) {
- binding.tilContainerSearch.setError(getString(stringResourceId));
- }
- }
-
- @Override
- public void setCategories(final List categories) {
- if (categories == null) {
- adapter.clear();
- } else {
- adapter.setItems(categories);
- }
- adapter.notifyDataSetChanged();
-
-
- if (binding == null) {
- return;
- }
- // Nested waiting for search result data to load into the category
- // list and smoothly scroll to the top of the search result list.
- binding.rvCategories.post(new Runnable() {
- @Override
- public void run() {
- binding.rvCategories.smoothScrollToPosition(0);
- binding.rvCategories.post(new Runnable() {
- @Override
- public void run() {
- binding.rvCategories.smoothScrollToPosition(0);
- }
- });
- }
- });
- }
-
- @Override
- public void goToNextScreen() {
- if (callback != null){
- callback.onNextButtonClicked(callback.getIndexInViewFlipper(this));
- }
- }
-
- @Override
- public void showNoCategorySelected() {
- if (media == null) {
- DialogUtil.showAlertDialog(requireActivity(),
- getString(R.string.no_categories_selected),
- getString(R.string.no_categories_selected_warning_desc),
- getString(R.string.continue_message),
- getString(R.string.cancel),
- this::goToNextScreen,
- null);
- } else {
- Toast.makeText(requireContext(), getString(R.string.no_categories_selected),
- Toast.LENGTH_SHORT).show();
- presenter.clearPreviousSelection();
- goBackToPreviousScreen();
- }
-
- }
-
- /**
- * Gets existing categories from media
- */
- @Override
- public List getExistingCategories() {
- return (media == null) ? null : media.getCategories();
- }
-
- /**
- * Returns required context
- */
- @NonNull
- @Override
- public Context getFragmentContext() {
- return requireContext();
- }
-
- /**
- * Returns to previous fragment
- */
- @Override
- public void goBackToPreviousScreen() {
- getFragmentManager().popBackStack();
- }
-
- /**
- * Shows the progress dialog
- */
- @Override
- public void showProgressDialog() {
- progressDialog = new ProgressDialog(requireContext());
- progressDialog.setMessage(getString(R.string.please_wait));
- progressDialog.show();
- }
-
- /**
- * Hides the progress dialog
- */
- @Override
- public void dismissProgressDialog() {
- if (progressDialog != null) {
- progressDialog.dismiss();
- }
- }
-
- /**
- * Refreshes the categories
- */
- @Override
- public void refreshCategories() {
- final MediaDetailFragment mediaDetailFragment = (MediaDetailFragment) getParentFragment();
- assert mediaDetailFragment != null;
- mediaDetailFragment.updateCategories();
- }
-
- /**
- *
- */
- @Override
- public void navigateToLoginScreen() {
- final String username = sessionManager.getUserName();
- final CommonsApplication.BaseLogoutListener logoutListener = new CommonsApplication.BaseLogoutListener(
- requireActivity(),
- requireActivity().getString(R.string.invalid_login_message),
- username
- );
-
- CommonsApplication.getInstance().clearApplicationData(
- requireActivity(), logoutListener);
- }
-
- public void onNextButtonClicked() {
- if (media != null) {
- presenter.updateCategories(media, wikiText);
- } else {
- presenter.verifyCategories();
- }
- }
-
- public void onPreviousButtonClicked() {
- if (media != null) {
- presenter.clearPreviousSelection();
- adapter.setItems(null);
- final MediaDetailFragment mediaDetailFragment = (MediaDetailFragment) getParentFragment();
- assert mediaDetailFragment != null;
- mediaDetailFragment.onResume();
- goBackToPreviousScreen();
- } else {
- if (callback != null) {
- callback.onPreviousButtonClicked(callback.getIndexInViewFlipper(this));
- }
- }
- }
-
- @Override
- protected void onBecameVisible() {
- super.onBecameVisible();
- if (binding == null) {
- return;
- }
- presenter.selectCategories();
- final Editable text = binding.etSearch.getText();
- if (text != null) {
- presenter.searchForCategories(text.toString());
- }
- }
-
- /**
- * Hides the action bar while opening editing fragment
- */
- @Override
- public void onResume() {
- super.onResume();
-
- if (media != null) {
- binding.etSearch.setOnKeyListener((v, keyCode, event) -> {
- if (keyCode == KeyEvent.KEYCODE_BACK) {
- binding.etSearch.clearFocus();
- presenter.clearPreviousSelection();
- final MediaDetailFragment mediaDetailFragment = (MediaDetailFragment) getParentFragment();
- assert mediaDetailFragment != null;
- mediaDetailFragment.onResume();
- goBackToPreviousScreen();
- return true;
- }
- return false;
- });
-
- requireView().setFocusableInTouchMode(true);
- getView().requestFocus();
- getView().setOnKeyListener((v, keyCode, event) -> {
- if (event.getAction() == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) {
- presenter.clearPreviousSelection();
- final MediaDetailFragment mediaDetailFragment = (MediaDetailFragment) getParentFragment();
- assert mediaDetailFragment != null;
- mediaDetailFragment.onResume();
- goBackToPreviousScreen();
- return true;
- }
- return false;
- });
-
- Objects.requireNonNull(
- ((AppCompatActivity) requireActivity()).getSupportActionBar())
- .hide();
-
- if (getParentFragment().getParentFragment().getParentFragment()
- instanceof ContributionsFragment) {
- ((ContributionsFragment) (getParentFragment()
- .getParentFragment().getParentFragment())).binding.cardViewNearby
- .setVisibility(View.GONE);
- }
- }
- }
-
- /**
- * Shows the action bar while closing editing fragment
- */
- @Override
- public void onStop() {
- super.onStop();
- if (media != null) {
- Objects.requireNonNull(
- ((AppCompatActivity) requireActivity()).getSupportActionBar())
- .show();
- }
- }
-
- @Override
- public void onDestroy() {
- super.onDestroy();
- binding = null;
- }
-}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/categories/UploadCategoriesFragment.kt b/app/src/main/java/fr/free/nrw/commons/upload/categories/UploadCategoriesFragment.kt
new file mode 100644
index 0000000000..efec296422
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/upload/categories/UploadCategoriesFragment.kt
@@ -0,0 +1,397 @@
+package fr.free.nrw.commons.upload.categories
+
+import android.app.Activity
+import android.app.ProgressDialog
+import android.content.Context
+import android.os.Bundle
+import android.view.KeyEvent
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.Toast
+import androidx.appcompat.app.AppCompatActivity
+import androidx.recyclerview.widget.LinearLayoutManager
+import com.jakewharton.rxbinding2.view.RxView
+import com.jakewharton.rxbinding2.widget.RxTextView
+import fr.free.nrw.commons.CommonsApplication
+import fr.free.nrw.commons.CommonsApplication.Companion.instance
+import fr.free.nrw.commons.Media
+import fr.free.nrw.commons.R
+import fr.free.nrw.commons.auth.SessionManager
+import fr.free.nrw.commons.category.CategoryItem
+import fr.free.nrw.commons.contributions.ContributionsFragment
+import fr.free.nrw.commons.databinding.UploadCategoriesFragmentBinding
+import fr.free.nrw.commons.media.MediaDetailFragment
+import fr.free.nrw.commons.upload.UploadActivity
+import fr.free.nrw.commons.upload.UploadBaseFragment
+import fr.free.nrw.commons.utils.DialogUtil.showAlertDialog
+import fr.free.nrw.commons.wikidata.WikidataConstants.SELECTED_NEARBY_PLACE_CATEGORY
+import io.reactivex.Notification
+import io.reactivex.android.schedulers.AndroidSchedulers
+import io.reactivex.disposables.Disposable
+import timber.log.Timber
+import java.util.Objects
+import java.util.concurrent.TimeUnit
+import javax.inject.Inject
+
+class UploadCategoriesFragment : UploadBaseFragment(), CategoriesContract.View {
+ @JvmField
+ @Inject
+ var presenter: CategoriesContract.UserActionListener? = null
+
+ @JvmField
+ @Inject
+ var sessionManager: SessionManager? = null
+ private var adapter: UploadCategoryAdapter? = null
+ private var subscribe: Disposable? = null
+
+ /**
+ * Current media
+ */
+ private var media: Media? = null
+
+ /**
+ * Progress Dialog for showing background process
+ */
+ private var progressDialog: ProgressDialog? = null
+
+ /**
+ * WikiText from the server
+ */
+ private var wikiText: String? = null
+ private var nearbyPlaceCategory: String? = null
+
+ private var binding: UploadCategoriesFragmentBinding? = null
+
+ override fun onCreateView(
+ inflater: LayoutInflater, container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ binding = UploadCategoriesFragmentBinding.inflate(inflater, container, false)
+ return binding!!.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ val bundle = arguments
+ if (bundle != null) {
+ media = bundle.getParcelable("Existing_Categories")
+ wikiText = bundle.getString("WikiText")
+ nearbyPlaceCategory = bundle.getString(SELECTED_NEARBY_PLACE_CATEGORY)
+ }
+ init()
+ presenter!!.getCategories().observe(
+ viewLifecycleOwner
+ ) { categories: List? ->
+ this.setCategories(
+ categories
+ )
+ }
+ }
+
+ private fun init() {
+ if (binding == null) {
+ return
+ }
+ if (media == null) {
+ if (callback != null) {
+ binding!!.tvTitle.text = getString(
+ R.string.step_count, callback.getIndexInViewFlipper(
+ this
+ ) + 1,
+ callback.totalNumberOfSteps, getString(R.string.categories_activity_title)
+ )
+ }
+ } else {
+ binding!!.tvTitle.setText(R.string.edit_categories)
+ binding!!.tvSubtitle.visibility = View.GONE
+ binding!!.btnNext.setText(R.string.menu_save_categories)
+ binding!!.btnPrevious.setText(R.string.menu_cancel_upload)
+ }
+
+ setTvSubTitle()
+ binding!!.tooltip.setOnClickListener {
+ showAlertDialog(
+ requireActivity(),
+ getString(R.string.categories_activity_title),
+ getString(R.string.categories_tooltip),
+ getString(android.R.string.ok),
+ null
+ )
+ }
+ if (media == null) {
+ presenter!!.onAttachView(this)
+ } else {
+ presenter!!.onAttachViewWithMedia(this, media!!)
+ }
+ binding!!.btnNext.setOnClickListener { v: View? -> onNextButtonClicked() }
+ binding!!.btnPrevious.setOnClickListener { v: View? -> onPreviousButtonClicked() }
+
+ initRecyclerView()
+ addTextChangeListenerToEtSearch()
+ }
+
+ private fun addTextChangeListenerToEtSearch() {
+ if (binding == null) {
+ return
+ }
+ subscribe = RxTextView.textChanges(binding!!.etSearch)
+ .doOnEach { v: Notification? ->
+ binding!!.tilContainerSearch.error =
+ null
+ }
+ .takeUntil(RxView.detaches(binding!!.etSearch))
+ .debounce(500, TimeUnit.MILLISECONDS)
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(
+ { filter: CharSequence -> searchForCategory(filter.toString()) },
+ { t: Throwable? -> Timber.e(t) })
+ }
+
+ /**
+ * Removes the tv subtitle If the activity is the instance of [UploadActivity] and
+ * if multiple files aren't selected.
+ */
+ private fun setTvSubTitle() {
+ val activity: Activity? = activity
+ if (activity is UploadActivity) {
+ val isMultipleFileSelected = activity.isMultipleFilesSelected
+ if (!isMultipleFileSelected) {
+ binding!!.tvSubtitle.visibility = View.GONE
+ }
+ }
+ }
+
+ private fun searchForCategory(query: String) {
+ presenter!!.searchForCategories(query)
+ }
+
+ private fun initRecyclerView() {
+ adapter = UploadCategoryAdapter({ categoryItem: CategoryItem? ->
+ presenter!!.onCategoryItemClicked(categoryItem!!)
+ Unit
+ }, nearbyPlaceCategory)
+
+ if (binding != null) {
+ binding!!.rvCategories.layoutManager = LinearLayoutManager(context)
+ binding!!.rvCategories.adapter = adapter
+ }
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+ presenter!!.onDetachView()
+ subscribe!!.dispose()
+ }
+
+ override fun showProgress(shouldShow: Boolean) {
+ binding?.pbCategories?.setVisibility(if (shouldShow) View.VISIBLE else View.GONE)
+ }
+
+ override fun showError(error: String?) {
+ binding?.tilContainerSearch?.error = error
+ }
+
+ override fun showError(stringResourceId: Int) {
+ binding?.tilContainerSearch?.error = getString(stringResourceId)
+ }
+
+ override fun setCategories(categories: List?) {
+ if (categories == null) {
+ adapter!!.clear()
+ } else {
+ adapter!!.items = categories
+ }
+ adapter!!.notifyDataSetChanged()
+
+ if (binding == null) {
+ return
+ }
+ // Nested waiting for search result data to load into the category
+ // list and smoothly scroll to the top of the search result list.
+ binding!!.rvCategories.post {
+ binding!!.rvCategories.smoothScrollToPosition(0)
+ binding!!.rvCategories.post {
+ binding!!.rvCategories.smoothScrollToPosition(
+ 0
+ )
+ }
+ }
+ }
+
+ override fun goToNextScreen() {
+ callback.onNextButtonClicked(callback.getIndexInViewFlipper(this))
+ }
+
+ override fun showNoCategorySelected() {
+ if (media == null) {
+ showAlertDialog(
+ requireActivity(),
+ getString(R.string.no_categories_selected),
+ getString(R.string.no_categories_selected_warning_desc),
+ getString(R.string.continue_message),
+ getString(R.string.cancel),
+ { this.goToNextScreen() },
+ null
+ )
+ } else {
+ Toast.makeText(
+ requireContext(), getString(R.string.no_categories_selected),
+ Toast.LENGTH_SHORT
+ ).show()
+ presenter!!.clearPreviousSelection()
+ goBackToPreviousScreen()
+ }
+ }
+
+ /**
+ * Gets existing categories from media
+ */
+ override fun getExistingCategories(): List? {
+ return media?.categories
+ }
+
+ /**
+ * Returns required context
+ */
+ override fun getFragmentContext(): Context {
+ return requireContext()
+ }
+
+ /**
+ * Returns to previous fragment
+ */
+ override fun goBackToPreviousScreen() {
+ fragmentManager?.popBackStack()
+ }
+
+ /**
+ * Shows the progress dialog
+ */
+ override fun showProgressDialog() {
+ progressDialog = ProgressDialog(requireContext()).apply {
+ setMessage(getString(R.string.please_wait))
+ }.also {
+ it.show()
+ }
+ }
+
+ /**
+ * Hides the progress dialog
+ */
+ override fun dismissProgressDialog() {
+ progressDialog?.dismiss()
+ }
+
+ /**
+ * Refreshes the categories
+ */
+ override fun refreshCategories() {
+ (parentFragment as MediaDetailFragment?)?.updateCategories()
+ }
+
+ /**
+ *
+ */
+ override fun navigateToLoginScreen() {
+ val username = sessionManager!!.userName
+ val logoutListener = CommonsApplication.BaseLogoutListener(
+ requireActivity(),
+ requireActivity().getString(R.string.invalid_login_message),
+ username
+ )
+
+ instance.clearApplicationData(
+ requireActivity(), logoutListener
+ )
+ }
+
+ fun onNextButtonClicked() {
+ if (media != null) {
+ presenter!!.updateCategories(media!!, wikiText!!)
+ } else {
+ presenter!!.verifyCategories()
+ }
+ }
+
+ fun onPreviousButtonClicked() {
+ if (media != null) {
+ presenter!!.clearPreviousSelection()
+ adapter!!.items = null
+ val mediaDetailFragment = checkNotNull(parentFragment as MediaDetailFragment?)
+ mediaDetailFragment.onResume()
+ goBackToPreviousScreen()
+ } else {
+ callback.onPreviousButtonClicked(callback.getIndexInViewFlipper(this))
+ }
+ }
+
+ override fun onBecameVisible() {
+ super.onBecameVisible()
+ if (binding == null) {
+ return
+ }
+ presenter!!.selectCategories()
+ val text = binding!!.etSearch.text
+ if (text != null) {
+ presenter!!.searchForCategories(text.toString())
+ }
+ }
+
+ /**
+ * Hides the action bar while opening editing fragment
+ */
+ override fun onResume() {
+ super.onResume()
+
+ if (media != null) {
+ binding!!.etSearch.setOnKeyListener { v: View?, keyCode: Int, event: KeyEvent? ->
+ if (keyCode == KeyEvent.KEYCODE_BACK) {
+ binding!!.etSearch.clearFocus()
+ presenter!!.clearPreviousSelection()
+ val mediaDetailFragment =
+ checkNotNull(parentFragment as MediaDetailFragment?)
+ mediaDetailFragment.onResume()
+ goBackToPreviousScreen()
+ return@setOnKeyListener true
+ }
+ false
+ }
+
+ requireView().isFocusableInTouchMode = true
+ requireView().requestFocus()
+ requireView().setOnKeyListener { v: View?, keyCode: Int, event: KeyEvent ->
+ if (event.action == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) {
+ presenter!!.clearPreviousSelection()
+ val mediaDetailFragment =
+ checkNotNull(parentFragment as MediaDetailFragment?)
+ mediaDetailFragment.onResume()
+ goBackToPreviousScreen()
+ return@setOnKeyListener true
+ }
+ false
+ }
+
+ (requireActivity() as AppCompatActivity).supportActionBar?.hide()
+
+ if (parentFragment?.parentFragment?.parentFragment is ContributionsFragment) {
+ ((parentFragment?.parentFragment?.parentFragment) as ContributionsFragment).binding.cardViewNearby.visibility = View.GONE
+ }
+ }
+ }
+
+ /**
+ * Shows the action bar while closing editing fragment
+ */
+ override fun onStop() {
+ super.onStop()
+ if (media != null) {
+ (requireActivity() as AppCompatActivity).supportActionBar?.show()
+ }
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ binding = null
+ }
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsFragment.java b/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsFragment.java
deleted file mode 100644
index b5a22a6224..0000000000
--- a/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsFragment.java
+++ /dev/null
@@ -1,444 +0,0 @@
-package fr.free.nrw.commons.upload.depicts;
-
-import static fr.free.nrw.commons.wikidata.WikidataConstants.SELECTED_NEARBY_PLACE;
-
-import android.app.Activity;
-import android.app.ProgressDialog;
-import android.content.Context;
-import android.os.Bundle;
-import android.view.KeyEvent;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.Toast;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.appcompat.app.AppCompatActivity;
-import androidx.recyclerview.widget.LinearLayoutManager;
-import com.jakewharton.rxbinding2.view.RxView;
-import com.jakewharton.rxbinding2.widget.RxTextView;
-import fr.free.nrw.commons.CommonsApplication;
-import fr.free.nrw.commons.Media;
-import fr.free.nrw.commons.R;
-import fr.free.nrw.commons.auth.SessionManager;
-import fr.free.nrw.commons.contributions.ContributionsFragment;
-import fr.free.nrw.commons.databinding.UploadDepictsFragmentBinding;
-import fr.free.nrw.commons.kvstore.JsonKvStore;
-import fr.free.nrw.commons.media.MediaDetailFragment;
-import fr.free.nrw.commons.nearby.Place;
-import fr.free.nrw.commons.upload.UploadActivity;
-import fr.free.nrw.commons.upload.UploadBaseFragment;
-import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
-import fr.free.nrw.commons.utils.DialogUtil;
-import io.reactivex.android.schedulers.AndroidSchedulers;
-import io.reactivex.disposables.Disposable;
-import java.util.List;
-import java.util.Objects;
-import java.util.concurrent.TimeUnit;
-import javax.inject.Inject;
-import javax.inject.Named;
-import kotlin.Unit;
-import timber.log.Timber;
-
-
-/**
- * Fragment for showing depicted items list in Upload activity after media details
- */
-public class DepictsFragment extends UploadBaseFragment implements DepictsContract.View {
-
- @Inject
- @Named("default_preferences")
- public
- JsonKvStore applicationKvStore;
-
- @Inject
- DepictsContract.UserActionListener presenter;
- private UploadDepictsAdapter adapter;
- private Disposable subscribe;
- private Media media;
- private ProgressDialog progressDialog;
- /**
- * Determines each encounter of edit depicts
- */
- private int count;
- private Place nearbyPlace;
-
- private UploadDepictsFragmentBinding binding;
-
- @Inject
- SessionManager sessionManager;
-
- @Nullable
- @Override
- public android.view.View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
- @Nullable Bundle savedInstanceState) {
- binding = UploadDepictsFragmentBinding.inflate(inflater, container, false);
- return binding.getRoot();
- }
-
- @Override
- public void onViewCreated(@NonNull android.view.View view, @Nullable Bundle savedInstanceState) {
- super.onViewCreated(view, savedInstanceState);
-
- Bundle bundle = getArguments();
- if (bundle != null) {
- media = bundle.getParcelable("Existing_Depicts");
- nearbyPlace = bundle.getParcelable(SELECTED_NEARBY_PLACE);
- }
-
- if(callback!=null || media!=null){
- init();
- presenter.getDepictedItems().observe(getViewLifecycleOwner(), this::setDepictsList);
- }
- }
-
- /**
- * Initialize presenter and views
- */
- private void init() {
-
- if (binding == null) {
- return;
- }
-
- if (media == null) {
- binding.depictsTitle.setText(String.format(getString(R.string.step_count), callback.getIndexInViewFlipper(this) + 1,
- callback.getTotalNumberOfSteps(), getString(R.string.depicts_step_title)));
- } else {
- binding.depictsTitle.setText(R.string.edit_depictions);
- binding.depictsSubtitle.setVisibility(View.GONE);
- binding.depictsNext.setText(R.string.menu_save_categories);
- binding.depictsPrevious.setText(R.string.menu_cancel_upload);
- }
-
- setDepictsSubTitle();
- binding.tooltip.setOnClickListener(v -> DialogUtil
- .showAlertDialog(getActivity(), getString(R.string.depicts_step_title),
- getString(R.string.depicts_tooltip), getString(android.R.string.ok), null));
- if (media == null) {
- presenter.onAttachView(this);
- } else {
- presenter.onAttachViewWithMedia(this, media);
- }
- initRecyclerView();
- addTextChangeListenerToSearchBox();
-
- binding.depictsNext.setOnClickListener(v->onNextButtonClicked());
- binding.depictsPrevious.setOnClickListener(v->onPreviousButtonClicked());
- }
-
- /**
- * Removes the depicts subtitle If the activity is the instance of [UploadActivity] and
- * if multiple files aren't selected.
- */
- private void setDepictsSubTitle() {
- final Activity activity = getActivity();
- if (activity instanceof UploadActivity) {
- final boolean isMultipleFileSelected = ((UploadActivity) activity).getIsMultipleFilesSelected();
- if (!isMultipleFileSelected) {
- binding.depictsSubtitle.setVisibility(View.GONE);
- }
- }
- }
-
- /**
- * Initialise recyclerView and set adapter
- */
- private void initRecyclerView() {
- if (media == null) {
- adapter = new UploadDepictsAdapter(categoryItem -> {
- presenter.onDepictItemClicked(categoryItem);
- return Unit.INSTANCE;
- }, nearbyPlace);
- } else {
- adapter = new UploadDepictsAdapter(item -> {
- presenter.onDepictItemClicked(item);
- return Unit.INSTANCE;
- }, nearbyPlace);
- }
- if (binding == null) {
- return;
- }
- binding.depictsRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
- binding.depictsRecyclerView.setAdapter(adapter);
- }
-
- @Override
- protected void onBecameVisible() {
- super.onBecameVisible();
- // Select Place depiction as the fragment becomes visible to ensure that the most up to date
- // Place is used (i.e. if the user accepts a nearby place dialog)
- presenter.selectPlaceDepictions();
- }
-
- @Override
- public void goToNextScreen() {
- callback.onNextButtonClicked(callback.getIndexInViewFlipper(this));
- }
-
- @Override
- public void goToPreviousScreen() {
- callback.onPreviousButtonClicked(callback.getIndexInViewFlipper(this));
- }
-
- @Override
- public void noDepictionSelected() {
- if (media == null) {
- DialogUtil.showAlertDialog(getActivity(),
- getString(R.string.no_depictions_selected),
- getString(R.string.no_depictions_selected_warning_desc),
- getString(R.string.continue_message),
- getString(R.string.cancel),
- this::goToNextScreen,
- null
- );
- } else {
- Toast.makeText(requireContext(), getString(R.string.no_depictions_selected),
- Toast.LENGTH_SHORT).show();
- presenter.clearPreviousSelection();
- updateDepicts();
- goBackToPreviousScreen();
- }
- }
-
- @Override
- public void onDestroyView() {
- super.onDestroyView();
- media = null;
- presenter.onDetachView();
- subscribe.dispose();
- }
-
- @Override
- public void showProgress(boolean shouldShow) {
- if (binding == null) {
- return;
- }
- binding.depictsSearchInProgress.setVisibility(shouldShow ? View.VISIBLE : View.GONE);
- }
-
- @Override
- public void showError(boolean value) {
- if (binding == null) {
- return;
- }
- if (value) {
- binding.depictsSearchContainer.setError(getString(R.string.no_depiction_found));
- } else {
- binding.depictsSearchContainer.setErrorEnabled(false);
- }
- }
-
- @Override
- public void setDepictsList(List depictedItemList) {
-
- if (applicationKvStore.getBoolean("first_edit_depict")) {
- count = 1;
- applicationKvStore.putBoolean("first_edit_depict", false);
- adapter.setItems(depictedItemList);
- } else {
- if ((count == 0) && (!depictedItemList.isEmpty())) {
- adapter.setItems(null);
- count = 1;
- } else {
- adapter.setItems(depictedItemList);
- }
- }
-
- if (binding == null) {
- return;
- }
- // Nested waiting for search result data to load into the depicted item
- // list and smoothly scroll to the top of the search result list.
- binding.depictsRecyclerView.post(new Runnable() {
- @Override
- public void run() {
- binding.depictsRecyclerView.smoothScrollToPosition(0);
- binding.depictsRecyclerView.post(new Runnable() {
- @Override
- public void run() {
- binding.depictsRecyclerView.smoothScrollToPosition(0);
- }
- });
- }
- });
- }
-
- /**
- * Returns required context
- */
- @Override
- public Context getFragmentContext(){
- return requireContext();
- }
-
- /**
- * Returns to previous fragment
- */
- @Override
- public void goBackToPreviousScreen() {
- getFragmentManager().popBackStack();
- }
-
- /**
- * Gets existing depictions IDs from media
- */
- @Override
- public List getExistingDepictions(){
- return (media == null) ? null : media.getDepictionIds();
- }
-
- /**
- * Shows the progress dialog
- */
- @Override
- public void showProgressDialog() {
- progressDialog = new ProgressDialog(requireContext());
- progressDialog.setMessage(getString(R.string.please_wait));
- progressDialog.show();
- }
-
- /**
- * Hides the progress dialog
- */
- @Override
- public void dismissProgressDialog() {
- progressDialog.dismiss();
- }
-
- /**
- * Update the depicts
- */
- @Override
- public void updateDepicts() {
- final MediaDetailFragment mediaDetailFragment = (MediaDetailFragment) getParentFragment();
- assert mediaDetailFragment != null;
- mediaDetailFragment.onResume();
- }
-
- /**
- * Navigates to the login Activity
- */
- @Override
- public void navigateToLoginScreen() {
- final String username = sessionManager.getUserName();
- final CommonsApplication.BaseLogoutListener logoutListener = new CommonsApplication.BaseLogoutListener(
- getActivity(),
- requireActivity().getString(R.string.invalid_login_message),
- username
- );
-
- CommonsApplication.getInstance().clearApplicationData(
- requireActivity(), logoutListener);
- }
-
- /**
- * Determines the calling fragment by media nullability and act accordingly
- */
- public void onNextButtonClicked() {
- if(media != null){
- presenter.updateDepictions(media);
- } else {
- presenter.verifyDepictions();
- }
- }
-
- /**
- * Determines the calling fragment by media nullability and act accordingly
- */
- public void onPreviousButtonClicked() {
- if(media != null){
- presenter.clearPreviousSelection();
- updateDepicts();
- goBackToPreviousScreen();
- } else {
- callback.onPreviousButtonClicked(callback.getIndexInViewFlipper(this));
- }
- }
-
- /**
- * Text change listener for the edit text view of depicts
- */
- private void addTextChangeListenerToSearchBox() {
- subscribe = RxTextView.textChanges(binding.depictsSearch)
- .doOnEach(v -> binding.depictsSearchContainer.setError(null))
- .takeUntil(RxView.detaches(binding.depictsSearch))
- .debounce(500, TimeUnit.MILLISECONDS)
- .observeOn(AndroidSchedulers.mainThread())
- .subscribe(filter -> searchForDepictions(filter.toString()), Timber::e);
- }
-
- /**
- * Search for depictions for the following query
- *
- * @param query query string
- */
- private void searchForDepictions(final String query) {
- presenter.searchForDepictions(query);
- }
-
-
-
- /**
- * Hides the action bar while opening editing fragment
- */
- @Override
- public void onResume() {
- super.onResume();
-
- if (media != null) {
- binding.depictsSearch.setOnKeyListener((v, keyCode, event) -> {
- if (keyCode == KeyEvent.KEYCODE_BACK) {
- binding.depictsSearch.clearFocus();
- presenter.clearPreviousSelection();
- updateDepicts();
- goBackToPreviousScreen();
- return true;
- }
- return false;
- });
-
- requireView().setFocusableInTouchMode(true);
- getView().requestFocus();
- getView().setOnKeyListener((v, keyCode, event) -> {
- if (event.getAction() == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) {
- presenter.clearPreviousSelection();
- updateDepicts();
- goBackToPreviousScreen();
- return true;
- }
- return false;
- });
-
- Objects.requireNonNull(
- ((AppCompatActivity) requireActivity()).getSupportActionBar())
- .hide();
-
- if (getParentFragment().getParentFragment().getParentFragment()
- instanceof ContributionsFragment) {
- ((ContributionsFragment) (getParentFragment()
- .getParentFragment().getParentFragment())).binding.cardViewNearby
- .setVisibility(View.GONE);
- }
- }
- }
-
- /**
- * Shows the action bar while closing editing fragment
- */
- @Override
- public void onStop() {
- super.onStop();
- if (media != null) {
- Objects.requireNonNull(
- ((AppCompatActivity) requireActivity()).getSupportActionBar())
- .show();
- }
- }
-
- @Override
- public void onDestroy() {
- super.onDestroy();
- binding = null;
- }
-}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsFragment.kt b/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsFragment.kt
new file mode 100644
index 0000000000..692e8422eb
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsFragment.kt
@@ -0,0 +1,424 @@
+package fr.free.nrw.commons.upload.depicts
+
+import android.app.Activity
+import android.app.ProgressDialog
+import android.content.Context
+import android.os.Bundle
+import android.view.KeyEvent
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.Toast
+import androidx.appcompat.app.AppCompatActivity
+import androidx.recyclerview.widget.LinearLayoutManager
+import com.jakewharton.rxbinding2.view.RxView
+import com.jakewharton.rxbinding2.widget.RxTextView
+import fr.free.nrw.commons.CommonsApplication
+import fr.free.nrw.commons.CommonsApplication.Companion.instance
+import fr.free.nrw.commons.Media
+import fr.free.nrw.commons.R
+import fr.free.nrw.commons.auth.SessionManager
+import fr.free.nrw.commons.contributions.ContributionsFragment
+import fr.free.nrw.commons.databinding.UploadDepictsFragmentBinding
+import fr.free.nrw.commons.kvstore.JsonKvStore
+import fr.free.nrw.commons.media.MediaDetailFragment
+import fr.free.nrw.commons.nearby.Place
+import fr.free.nrw.commons.upload.UploadActivity
+import fr.free.nrw.commons.upload.UploadBaseFragment
+import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
+import fr.free.nrw.commons.utils.DialogUtil.showAlertDialog
+import fr.free.nrw.commons.wikidata.WikidataConstants.SELECTED_NEARBY_PLACE
+import io.reactivex.Notification
+import io.reactivex.android.schedulers.AndroidSchedulers
+import io.reactivex.disposables.Disposable
+import timber.log.Timber
+import java.util.concurrent.TimeUnit
+import javax.inject.Inject
+import javax.inject.Named
+
+/**
+ * Fragment for showing depicted items list in Upload activity after media details
+ */
+class DepictsFragment : UploadBaseFragment(), DepictsContract.View {
+ @Inject
+ @field:Named("default_preferences")
+ lateinit var applicationKvStore: JsonKvStore
+
+ @Inject
+ lateinit var presenter: DepictsContract.UserActionListener
+
+ @Inject
+ lateinit var sessionManager: SessionManager
+
+ private var adapter: UploadDepictsAdapter? = null
+ private var subscribe: Disposable? = null
+ private var media: Media? = null
+ private var progressDialog: ProgressDialog? = null
+
+ /**
+ * Determines each encounter of edit depicts
+ */
+ private var count = 0
+ private var nearbyPlace: Place? = null
+
+ private var _binding: UploadDepictsFragmentBinding? = null
+ private val binding get() = _binding!!
+
+ override fun onCreateView(
+ inflater: LayoutInflater, container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ _binding = UploadDepictsFragmentBinding.inflate(inflater, container, false)
+ return binding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ arguments?.let {
+ media = it.getParcelable("Existing_Depicts")
+ nearbyPlace = it.getParcelable(SELECTED_NEARBY_PLACE)
+ }
+
+ if (callback != null || media != null) {
+ init()
+ presenter.getDepictedItems().observe(viewLifecycleOwner, ::setDepictsList)
+ }
+ }
+
+ /**
+ * Initialize presenter and views
+ */
+ private fun init() {
+ if (_binding == null) {
+ return
+ }
+
+ if (media == null) {
+ binding.depictsTitle.text =
+ String.format(
+ getString(R.string.step_count), callback.getIndexInViewFlipper(
+ this
+ ) + 1,
+ callback.totalNumberOfSteps, getString(R.string.depicts_step_title)
+ )
+ } else {
+ binding.depictsTitle.setText(R.string.edit_depictions)
+ binding.depictsSubtitle.visibility = View.GONE
+ binding.depictsNext.setText(R.string.menu_save_categories)
+ binding.depictsPrevious.setText(R.string.menu_cancel_upload)
+ }
+
+ setDepictsSubTitle()
+ binding.tooltip.setOnClickListener { v: View? ->
+ showAlertDialog(
+ requireActivity(),
+ getString(R.string.depicts_step_title),
+ getString(R.string.depicts_tooltip),
+ getString(android.R.string.ok),
+ null
+ )
+ }
+ if (media == null) {
+ presenter.onAttachView(this)
+ } else {
+ presenter.onAttachViewWithMedia(this, media!!)
+ }
+ initRecyclerView()
+ addTextChangeListenerToSearchBox()
+
+ binding.depictsNext.setOnClickListener { v: View? -> onNextButtonClicked() }
+ binding.depictsPrevious.setOnClickListener { v: View? -> onPreviousButtonClicked() }
+ }
+
+ /**
+ * Removes the depicts subtitle If the activity is the instance of [UploadActivity] and
+ * if multiple files aren't selected.
+ */
+ private fun setDepictsSubTitle() {
+ val activity: Activity? = activity
+ if (activity is UploadActivity) {
+ val isMultipleFileSelected = activity.isMultipleFilesSelected
+ if (!isMultipleFileSelected) {
+ binding.depictsSubtitle.visibility = View.GONE
+ }
+ }
+ }
+
+ /**
+ * Initialise recyclerView and set adapter
+ */
+ private fun initRecyclerView() {
+ adapter = if (media == null) {
+ UploadDepictsAdapter({ categoryItem: DepictedItem? ->
+ presenter.onDepictItemClicked(categoryItem!!)
+ }, nearbyPlace)
+ } else {
+ UploadDepictsAdapter({ item: DepictedItem? ->
+ presenter.onDepictItemClicked(item!!)
+ }, nearbyPlace)
+ }
+ if (_binding == null) {
+ return
+ }
+ binding.depictsRecyclerView.layoutManager = LinearLayoutManager(context)
+ binding.depictsRecyclerView.adapter = adapter
+ }
+
+ override fun onBecameVisible() {
+ super.onBecameVisible()
+ // Select Place depiction as the fragment becomes visible to ensure that the most up to date
+ // Place is used (i.e. if the user accepts a nearby place dialog)
+ presenter.selectPlaceDepictions()
+ }
+
+ override fun goToNextScreen() {
+ callback.onNextButtonClicked(callback.getIndexInViewFlipper(this))
+ }
+
+ override fun goToPreviousScreen() {
+ callback.onPreviousButtonClicked(callback.getIndexInViewFlipper(this))
+ }
+
+ override fun noDepictionSelected() {
+ if (media == null) {
+ showAlertDialog(
+ requireActivity(),
+ getString(R.string.no_depictions_selected),
+ getString(R.string.no_depictions_selected_warning_desc),
+ getString(R.string.continue_message),
+ getString(R.string.cancel),
+ { goToNextScreen() },
+ null
+ )
+ } else {
+ Toast.makeText(
+ requireContext(), getString(R.string.no_depictions_selected),
+ Toast.LENGTH_SHORT
+ ).show()
+ presenter.clearPreviousSelection()
+ updateDepicts()
+ goBackToPreviousScreen()
+ }
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+ media = null
+ presenter.onDetachView()
+ subscribe!!.dispose()
+ }
+
+ override fun showProgress(shouldShow: Boolean) {
+ if (_binding == null) {
+ return
+ }
+ binding.depictsSearchInProgress.visibility =
+ if (shouldShow) View.VISIBLE else View.GONE
+ }
+
+ override fun showError(value: Boolean) {
+ if (_binding == null) {
+ return
+ }
+ if (value) {
+ binding.depictsSearchContainer.error =
+ getString(R.string.no_depiction_found)
+ } else {
+ binding.depictsSearchContainer.isErrorEnabled = false
+ }
+ }
+
+ override fun setDepictsList(depictedItemList: List) {
+ if (applicationKvStore.getBoolean("first_edit_depict")) {
+ count = 1
+ applicationKvStore.putBoolean("first_edit_depict", false)
+ adapter!!.items = depictedItemList
+ } else {
+ if ((count == 0) && (!depictedItemList.isEmpty())) {
+ adapter!!.items = null
+ count = 1
+ } else {
+ adapter!!.items = depictedItemList
+ }
+ }
+
+ if (_binding == null) {
+ return
+ }
+ // Nested waiting for search result data to load into the depicted item
+ // list and smoothly scroll to the top of the search result list.
+ binding.depictsRecyclerView.post {
+ binding.depictsRecyclerView.smoothScrollToPosition(0)
+ binding.depictsRecyclerView.post {
+ binding.depictsRecyclerView.smoothScrollToPosition(
+ 0
+ )
+ }
+ }
+ }
+
+ /**
+ * Returns required context
+ */
+ override fun getFragmentContext(): Context {
+ return requireContext()
+ }
+
+ /**
+ * Returns to previous fragment
+ */
+ override fun goBackToPreviousScreen() {
+ fragmentManager?.popBackStack()
+ }
+
+ /**
+ * Gets existing depictions IDs from media
+ */
+ override fun getExistingDepictions(): List? {
+ return if ((media == null)) null else media!!.depictionIds
+ }
+
+ /**
+ * Shows the progress dialog
+ */
+ override fun showProgressDialog() {
+ progressDialog = ProgressDialog(requireContext())
+ progressDialog!!.setMessage(getString(R.string.please_wait))
+ progressDialog!!.show()
+ }
+
+ /**
+ * Hides the progress dialog
+ */
+ override fun dismissProgressDialog() {
+ progressDialog?.dismiss()
+ }
+
+ /**
+ * Update the depicts
+ */
+ override fun updateDepicts() {
+ (parentFragment as MediaDetailFragment?)?.onResume()
+ }
+
+ /**
+ * Navigates to the login Activity
+ */
+ override fun navigateToLoginScreen() {
+ val username = sessionManager.userName
+ val logoutListener = CommonsApplication.BaseLogoutListener(
+ requireActivity(),
+ requireActivity().getString(R.string.invalid_login_message),
+ username
+ )
+
+ instance.clearApplicationData(
+ requireActivity(), logoutListener
+ )
+ }
+
+ /**
+ * Determines the calling fragment by media nullability and act accordingly
+ */
+ fun onNextButtonClicked() {
+ if (media != null) {
+ presenter.updateDepictions(media!!)
+ } else {
+ presenter.verifyDepictions()
+ }
+ }
+
+ /**
+ * Determines the calling fragment by media nullability and act accordingly
+ */
+ fun onPreviousButtonClicked() {
+ if (media != null) {
+ presenter.clearPreviousSelection()
+ updateDepicts()
+ goBackToPreviousScreen()
+ } else {
+ callback.onPreviousButtonClicked(callback.getIndexInViewFlipper(this))
+ }
+ }
+
+ /**
+ * Text change listener for the edit text view of depicts
+ */
+ private fun addTextChangeListenerToSearchBox() {
+ subscribe = RxTextView.textChanges(binding.depictsSearch)
+ .doOnEach { v: Notification? ->
+ binding.depictsSearchContainer.error =
+ null
+ }
+ .takeUntil(RxView.detaches(binding.depictsSearch))
+ .debounce(500, TimeUnit.MILLISECONDS)
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(
+ { filter: CharSequence -> searchForDepictions(filter.toString()) },
+ { t: Throwable? -> Timber.e(t) })
+ }
+
+ /**
+ * Search for depictions for the following query
+ *
+ * @param query query string
+ */
+ private fun searchForDepictions(query: String) {
+ presenter.searchForDepictions(query)
+ }
+
+
+ /**
+ * Hides the action bar while opening editing fragment
+ */
+ override fun onResume() {
+ super.onResume()
+
+ if (media != null) {
+ binding.depictsSearch.setOnKeyListener { v: View?, keyCode: Int, event: KeyEvent? ->
+ if (keyCode == KeyEvent.KEYCODE_BACK) {
+ binding.depictsSearch.clearFocus()
+ presenter.clearPreviousSelection()
+ updateDepicts()
+ goBackToPreviousScreen()
+ return@setOnKeyListener true
+ }
+ false
+ }
+
+ requireView().isFocusableInTouchMode = true
+ requireView().requestFocus()
+ requireView().setOnKeyListener { v: View?, keyCode: Int, event: KeyEvent ->
+ if (event.action == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) {
+ presenter.clearPreviousSelection()
+ updateDepicts()
+ goBackToPreviousScreen()
+ return@setOnKeyListener true
+ }
+ false
+ }
+
+ (requireActivity() as AppCompatActivity).supportActionBar?.hide()
+
+ if (parentFragment?.parentFragment?.parentFragment is ContributionsFragment) {
+ ((parentFragment?.parentFragment?.parentFragment) as ContributionsFragment?)?.binding?.cardViewNearby?.setVisibility(View.GONE)
+ }
+ }
+ }
+
+ /**
+ * Shows the action bar while closing editing fragment
+ */
+ override fun onStop() {
+ super.onStop()
+ if (media != null) {
+ (requireActivity() as AppCompatActivity).supportActionBar?.show()
+ }
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ _binding = null
+ }
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/license/MediaLicenseContract.kt b/app/src/main/java/fr/free/nrw/commons/upload/license/MediaLicenseContract.kt
index 27ec1521eb..b0ca4ebf24 100644
--- a/app/src/main/java/fr/free/nrw/commons/upload/license/MediaLicenseContract.kt
+++ b/app/src/main/java/fr/free/nrw/commons/upload/license/MediaLicenseContract.kt
@@ -11,13 +11,13 @@ interface MediaLicenseContract {
fun setSelectedLicense(license: String?)
- fun updateLicenseSummary(selectedLicense: String?, numberOfItems: Int?)
+ fun updateLicenseSummary(selectedLicense: String?, numberOfItems: Int)
}
interface UserActionListener : BasePresenter {
fun getLicenses()
- fun selectLicense(licenseName: String)
+ fun selectLicense(licenseName: String?)
fun isWLMSupportedForThisPlace(): Boolean
}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/license/MediaLicenseFragment.java b/app/src/main/java/fr/free/nrw/commons/upload/license/MediaLicenseFragment.java
deleted file mode 100644
index 5fb82f2f6a..0000000000
--- a/app/src/main/java/fr/free/nrw/commons/upload/license/MediaLicenseFragment.java
+++ /dev/null
@@ -1,205 +0,0 @@
-package fr.free.nrw.commons.upload.license;
-
-import android.app.Activity;
-import android.net.Uri;
-import android.os.Bundle;
-import android.text.Html;
-import android.text.SpannableStringBuilder;
-import android.text.method.LinkMovementMethod;
-import android.text.style.ClickableSpan;
-import android.text.style.URLSpan;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.AdapterView;
-import android.widget.AdapterView.OnItemSelectedListener;
-import android.widget.ArrayAdapter;
-import android.widget.TextView;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import fr.free.nrw.commons.databinding.FragmentMediaLicenseBinding;
-import fr.free.nrw.commons.upload.UploadActivity;
-import fr.free.nrw.commons.utils.DialogUtil;
-import java.util.List;
-
-import javax.inject.Inject;
-import fr.free.nrw.commons.R;
-import fr.free.nrw.commons.Utils;
-import fr.free.nrw.commons.upload.UploadBaseFragment;
-import timber.log.Timber;
-
-public class MediaLicenseFragment extends UploadBaseFragment implements MediaLicenseContract.View {
-
- @Inject
- MediaLicenseContract.UserActionListener presenter;
-
- private FragmentMediaLicenseBinding binding;
- private ArrayAdapter adapter;
- private List licenses;
-
- @Nullable
- @Override
- public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
- @Nullable Bundle savedInstanceState) {
- binding = FragmentMediaLicenseBinding.inflate(inflater, container, false);
- return binding.getRoot();
- }
-
- @Override
- public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
- super.onViewCreated(view, savedInstanceState);
-
- binding.tvTitle.setText(getString(R.string.step_count,
- callback.getIndexInViewFlipper(this) + 1,
- callback.getTotalNumberOfSteps(),
- getString(R.string.license_step_title))
- );
- setTvSubTitle();
- binding.btnPrevious.setOnClickListener(v ->
- callback.onPreviousButtonClicked(callback.getIndexInViewFlipper(this))
- );
-
- binding.btnSubmit.setOnClickListener(v ->
- callback.onNextButtonClicked(callback.getIndexInViewFlipper(this))
- );
-
- binding.tooltip.setOnClickListener(v ->
- DialogUtil.showAlertDialog(requireActivity(),
- getString(R.string.license_step_title),
- getString(R.string.license_tooltip),
- getString(android.R.string.ok),
- null)
- );
-
- initPresenter();
- initLicenseSpinner();
- presenter.getLicenses();
- }
-
- /**
- * Removes the tv Subtitle If the activity is the instance of [UploadActivity] and
- * if multiple files aren't selected.
- */
- private void setTvSubTitle() {
- final Activity activity = getActivity();
- if (activity instanceof UploadActivity) {
- final boolean isMultipleFileSelected = ((UploadActivity) activity).getIsMultipleFilesSelected();
- if (!isMultipleFileSelected) {
- binding.tvSubtitle.setVisibility(View.GONE);
- }
- }
- }
-
- private void initPresenter() {
- presenter.onAttachView(this);
- }
-
- /**
- * Initialise the license spinner
- */
- private void initLicenseSpinner() {
- if (getActivity() == null) {
- return;
- }
- adapter = new ArrayAdapter<>(getActivity().getApplicationContext(), android.R.layout.simple_spinner_dropdown_item);
- binding.spinnerLicenseList.setAdapter(adapter);
- binding.spinnerLicenseList.setOnItemSelectedListener(new OnItemSelectedListener() {
- @Override
- public void onItemSelected(AdapterView> adapterView, View view, int position,
- long l) {
- String licenseName = adapterView.getItemAtPosition(position).toString();
- presenter.selectLicense(licenseName);
- }
-
- @Override
- public void onNothingSelected(AdapterView> adapterView) {
- presenter.selectLicense(null);
- }
- });
- }
-
- @Override
- public void setLicenses(List licenses) {
- adapter.clear();
- this.licenses = licenses;
- adapter.addAll(this.licenses);
- adapter.notifyDataSetChanged();
- }
-
- @Override
- public void setSelectedLicense(String license) {
- int position = licenses.indexOf(getString(Utils.licenseNameFor(license)));
- // Check if position is valid
- if (position < 0) {
- Timber.d("Invalid position: %d. Using default licenses", position);
- position = licenses.size() - 1;
- } else {
- Timber.d("Position: %d %s", position, getString(Utils.licenseNameFor(license)));
- }
- binding.spinnerLicenseList.setSelection(position);
- }
-
- @Override
- public void updateLicenseSummary(String licenseSummary, Integer numberOfItems) {
- String licenseHyperLink = "" +
- getString(Utils.licenseNameFor(licenseSummary)) + "
";
-
- setTextViewHTML(binding.tvShareLicenseSummary, getResources()
- .getQuantityString(R.plurals.share_license_summary, numberOfItems,
- licenseHyperLink));
- }
-
- private void setTextViewHTML(TextView textView, String text) {
- CharSequence sequence = Html.fromHtml(text);
- SpannableStringBuilder strBuilder = new SpannableStringBuilder(sequence);
- URLSpan[] urls = strBuilder.getSpans(0, sequence.length(), URLSpan.class);
- for (URLSpan span : urls) {
- makeLinkClickable(strBuilder, span);
- }
- textView.setText(strBuilder);
- textView.setMovementMethod(LinkMovementMethod.getInstance());
- }
-
- private void makeLinkClickable(SpannableStringBuilder strBuilder, final URLSpan span) {
- int start = strBuilder.getSpanStart(span);
- int end = strBuilder.getSpanEnd(span);
- int flags = strBuilder.getSpanFlags(span);
- ClickableSpan clickable = new ClickableSpan() {
- @Override
- public void onClick(View view) {
- // Handle hyperlink click
- String hyperLink = span.getURL();
- launchBrowser(hyperLink);
- }
- };
- strBuilder.setSpan(clickable, start, end, flags);
- strBuilder.removeSpan(span);
- }
-
- private void launchBrowser(String hyperLink) {
- Utils.handleWebUrl(getContext(), Uri.parse(hyperLink));
- }
-
- @Override
- public void onDestroyView() {
- presenter.onDetachView();
- //Free the adapter to avoid memory leaks
- adapter = null;
- binding = null;
- super.onDestroyView();
- }
-
- @Override
- protected void onBecameVisible() {
- super.onBecameVisible();
- /**
- * Show the wlm info message if the upload is a WLM upload
- */
- if(callback.isWLMUpload() && presenter.isWLMSupportedForThisPlace()){
- binding.llInfoMonumentUpload.setVisibility(View.VISIBLE);
- }else{
- binding.llInfoMonumentUpload.setVisibility(View.GONE);
- }
- }
-}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/license/MediaLicenseFragment.kt b/app/src/main/java/fr/free/nrw/commons/upload/license/MediaLicenseFragment.kt
new file mode 100644
index 0000000000..656aee5def
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/upload/license/MediaLicenseFragment.kt
@@ -0,0 +1,201 @@
+package fr.free.nrw.commons.upload.license
+
+import android.app.Activity
+import android.net.Uri
+import android.os.Bundle
+import android.text.Html
+import android.text.SpannableStringBuilder
+import android.text.method.LinkMovementMethod
+import android.text.style.ClickableSpan
+import android.text.style.URLSpan
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.AdapterView
+import android.widget.ArrayAdapter
+import android.widget.TextView
+import fr.free.nrw.commons.R
+import fr.free.nrw.commons.Utils
+import fr.free.nrw.commons.databinding.FragmentMediaLicenseBinding
+import fr.free.nrw.commons.upload.UploadActivity
+import fr.free.nrw.commons.upload.UploadBaseFragment
+import fr.free.nrw.commons.utils.DialogUtil.showAlertDialog
+import timber.log.Timber
+import javax.inject.Inject
+
+class MediaLicenseFragment : UploadBaseFragment(), MediaLicenseContract.View {
+ @Inject
+ lateinit var presenter: MediaLicenseContract.UserActionListener
+
+ private var _binding: FragmentMediaLicenseBinding? = null
+ private val binding: FragmentMediaLicenseBinding get() = _binding!!
+
+ private var adapter: ArrayAdapter? = null
+ private var licenses: List? = null
+
+ override fun onCreateView(
+ inflater: LayoutInflater, container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ _binding = FragmentMediaLicenseBinding.inflate(inflater, container, false)
+ return binding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ binding.tvTitle.text = getString(
+ R.string.step_count,
+ callback.getIndexInViewFlipper(this) + 1,
+ callback.totalNumberOfSteps,
+ getString(R.string.license_step_title)
+ )
+ setTvSubTitle()
+ binding.btnPrevious.setOnClickListener {
+ callback.onPreviousButtonClicked(
+ callback.getIndexInViewFlipper(this)
+ )
+ }
+
+ binding.btnSubmit.setOnClickListener {
+ callback.onNextButtonClicked(
+ callback.getIndexInViewFlipper(this)
+ )
+ }
+
+ binding.tooltip.setOnClickListener {
+ showAlertDialog(
+ requireActivity(),
+ getString(R.string.license_step_title),
+ getString(R.string.license_tooltip),
+ getString(android.R.string.ok),
+ null
+ )
+ }
+
+ initPresenter()
+ initLicenseSpinner()
+ presenter.getLicenses()
+ }
+
+ /**
+ * Removes the tv Subtitle If the activity is the instance of [UploadActivity] and
+ * if multiple files aren't selected.
+ */
+ private fun setTvSubTitle() {
+ val activity: Activity? = activity
+ if (activity is UploadActivity) {
+ if (!activity.isMultipleFilesSelected) {
+ binding.tvSubtitle.visibility = View.GONE
+ }
+ }
+ }
+
+ private fun initPresenter() = presenter.onAttachView(this)
+
+ /**
+ * Initialise the license spinner
+ */
+ private fun initLicenseSpinner() {
+ if (activity == null) {
+ return
+ }
+ adapter = ArrayAdapter(
+ requireActivity().applicationContext,
+ android.R.layout.simple_spinner_dropdown_item
+ )
+ binding.spinnerLicenseList.adapter = adapter
+ binding.spinnerLicenseList.onItemSelectedListener =
+ object : AdapterView.OnItemSelectedListener {
+ override fun onItemSelected(adapterView: AdapterView<*>, view: View, position: Int, l: Long) {
+ val licenseName = adapterView.getItemAtPosition(position).toString()
+ presenter.selectLicense(licenseName)
+ }
+
+ override fun onNothingSelected(adapterView: AdapterView<*>?) {
+ presenter.selectLicense(null)
+ }
+ }
+ }
+
+ override fun setLicenses(licenses: List?) {
+ adapter!!.clear()
+ this.licenses = licenses
+ adapter!!.addAll(this.licenses!!)
+ adapter!!.notifyDataSetChanged()
+ }
+
+ override fun setSelectedLicense(license: String?) {
+ var position = licenses!!.indexOf(getString(Utils.licenseNameFor(license)))
+ // Check if position is valid
+ if (position < 0) {
+ Timber.d("Invalid position: %d. Using default licenses", position)
+ position = licenses!!.size - 1
+ } else {
+ Timber.d("Position: %d %s", position, getString(Utils.licenseNameFor(license)))
+ }
+ binding.spinnerLicenseList.setSelection(position)
+ }
+
+ override fun updateLicenseSummary(selectedLicense: String?, numberOfItems: Int) {
+ val licenseHyperLink = "" +
+ getString(Utils.licenseNameFor(selectedLicense)) + "
"
+
+ setTextViewHTML(
+ binding.tvShareLicenseSummary, resources
+ .getQuantityString(
+ R.plurals.share_license_summary, numberOfItems,
+ licenseHyperLink
+ )
+ )
+ }
+
+ private fun setTextViewHTML(textView: TextView, text: String) {
+ val sequence: CharSequence = Html.fromHtml(text)
+ val strBuilder = SpannableStringBuilder(sequence)
+ val urls = strBuilder.getSpans(
+ 0, sequence.length,
+ URLSpan::class.java
+ )
+ for (span in urls) {
+ makeLinkClickable(strBuilder, span)
+ }
+ textView.text = strBuilder
+ textView.movementMethod = LinkMovementMethod.getInstance()
+ }
+
+ private fun makeLinkClickable(strBuilder: SpannableStringBuilder, span: URLSpan) {
+ val start = strBuilder.getSpanStart(span)
+ val end = strBuilder.getSpanEnd(span)
+ val flags = strBuilder.getSpanFlags(span)
+ val clickable: ClickableSpan = object : ClickableSpan() {
+ override fun onClick(view: View) {
+ // Handle hyperlink click
+ val hyperLink = span.url
+ launchBrowser(hyperLink)
+ }
+ }
+ strBuilder.setSpan(clickable, start, end, flags)
+ strBuilder.removeSpan(span)
+ }
+
+ private fun launchBrowser(hyperLink: String) =
+ Utils.handleWebUrl(context, Uri.parse(hyperLink))
+
+ override fun onDestroyView() {
+ presenter.onDetachView()
+ //Free the adapter to avoid memory leaks
+ adapter = null
+ _binding = null
+ super.onDestroyView()
+ }
+
+ override fun onBecameVisible() {
+ super.onBecameVisible()
+ /**
+ * Show the wlm info message if the upload is a WLM upload
+ */
+ binding.llInfoMonumentUpload.visibility =
+ if (callback.isWLMUpload && presenter.isWLMSupportedForThisPlace()) View.VISIBLE else View.GONE
+ }
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/license/MediaLicensePresenter.java b/app/src/main/java/fr/free/nrw/commons/upload/license/MediaLicensePresenter.java
deleted file mode 100644
index 18955636e5..0000000000
--- a/app/src/main/java/fr/free/nrw/commons/upload/license/MediaLicensePresenter.java
+++ /dev/null
@@ -1,83 +0,0 @@
-package fr.free.nrw.commons.upload.license;
-
-import androidx.annotation.NonNull;
-import fr.free.nrw.commons.Utils;
-import fr.free.nrw.commons.kvstore.JsonKvStore;
-import fr.free.nrw.commons.repository.UploadRepository;
-import fr.free.nrw.commons.settings.Prefs;
-import fr.free.nrw.commons.upload.license.MediaLicenseContract.View;
-import java.lang.reflect.Proxy;
-import java.util.List;
-import javax.inject.Inject;
-import javax.inject.Named;
-import timber.log.Timber;
-
-/**
- * Added JavaDocs for MediaLicensePresenter
- */
-public class MediaLicensePresenter implements MediaLicenseContract.UserActionListener {
-
- private static final MediaLicenseContract.View DUMMY = (MediaLicenseContract.View) Proxy
- .newProxyInstance(
- MediaLicenseContract.View.class.getClassLoader(),
- new Class[]{MediaLicenseContract.View.class},
- (proxy, method, methodArgs) -> null);
-
- private final UploadRepository repository;
- private final JsonKvStore defaultKVStore;
- private MediaLicenseContract.View view = DUMMY;
-
- @Inject
- public MediaLicensePresenter(final UploadRepository uploadRepository,
- @Named("default_preferences") final JsonKvStore defaultKVStore) {
- this.repository = uploadRepository;
- this.defaultKVStore = defaultKVStore;
- }
-
- @Override
- public void onAttachView(@NonNull final View view) {
- this.view = view;
- }
-
- @Override
- public void onDetachView() {
- this.view = DUMMY;
- }
-
- /**
- * asks the repository for the available licenses, and informs the view on the same
- */
- @Override
- public void getLicenses() {
- final List licenses = repository.getLicenses();
- view.setLicenses(licenses);
-
- String selectedLicense = defaultKVStore.getString(Prefs.DEFAULT_LICENSE,
- Prefs.Licenses.CC_BY_SA_4);//CC_BY_SA_4 is the default one used by the commons web app
- try {//I have to make sure that the stored default license was not one of the deprecated one's
- Utils.licenseNameFor(selectedLicense);
- } catch (final IllegalStateException exception) {
- Timber.e(exception);
- selectedLicense = Prefs.Licenses.CC_BY_SA_4;
- defaultKVStore.putString(Prefs.DEFAULT_LICENSE, Prefs.Licenses.CC_BY_SA_4);
- }
- view.setSelectedLicense(selectedLicense);
-
- }
-
- /**
- * ask the repository to select a license for the current upload
- *
- * @param licenseName
- */
- @Override
- public void selectLicense(final String licenseName) {
- repository.setSelectedLicense(licenseName);
- view.updateLicenseSummary(repository.getSelectedLicense(), repository.getCount());
- }
-
- @Override
- public boolean isWLMSupportedForThisPlace() {
- return repository.isWMLSupportedForThisPlace();
- }
-}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/license/MediaLicensePresenter.kt b/app/src/main/java/fr/free/nrw/commons/upload/license/MediaLicensePresenter.kt
new file mode 100644
index 0000000000..25d1a23240
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/upload/license/MediaLicensePresenter.kt
@@ -0,0 +1,68 @@
+package fr.free.nrw.commons.upload.license
+
+import fr.free.nrw.commons.Utils
+import fr.free.nrw.commons.kvstore.JsonKvStore
+import fr.free.nrw.commons.repository.UploadRepository
+import fr.free.nrw.commons.settings.Prefs
+import timber.log.Timber
+import java.lang.reflect.Method
+import java.lang.reflect.Proxy
+import javax.inject.Inject
+import javax.inject.Named
+
+/**
+ * Added JavaDocs for MediaLicensePresenter
+ */
+class MediaLicensePresenter @Inject constructor(
+ private val repository: UploadRepository,
+ @param:Named("default_preferences") private val defaultKVStore: JsonKvStore
+) : MediaLicenseContract.UserActionListener {
+ private var view = DUMMY
+
+ override fun onAttachView(view: MediaLicenseContract.View) {
+ this.view = view
+ }
+
+ override fun onDetachView() {
+ view = DUMMY
+ }
+
+ /**
+ * asks the repository for the available licenses, and informs the view on the same
+ */
+ override fun getLicenses() {
+ val licenses = repository.getLicenses()
+ view.setLicenses(licenses)
+
+ var selectedLicense = defaultKVStore.getString(
+ Prefs.DEFAULT_LICENSE,
+ Prefs.Licenses.CC_BY_SA_4
+ ) //CC_BY_SA_4 is the default one used by the commons web app
+ try { //I have to make sure that the stored default license was not one of the deprecated one's
+ Utils.licenseNameFor(selectedLicense)
+ } catch (exception: IllegalStateException) {
+ Timber.e(exception)
+ selectedLicense = Prefs.Licenses.CC_BY_SA_4
+ defaultKVStore.putString(Prefs.DEFAULT_LICENSE, Prefs.Licenses.CC_BY_SA_4)
+ }
+ view.setSelectedLicense(selectedLicense)
+ }
+
+ /**
+ * ask the repository to select a license for the current upload
+ */
+ override fun selectLicense(licenseName: String?) {
+ repository.setSelectedLicense(licenseName)
+ view.updateLicenseSummary(repository.getSelectedLicense(), repository.getCount())
+ }
+
+ override fun isWLMSupportedForThisPlace(): Boolean =
+ repository.isWMLSupportedForThisPlace()
+
+ companion object {
+ private val DUMMY = Proxy.newProxyInstance(
+ MediaLicenseContract.View::class.java.classLoader,
+ arrayOf<*>>(MediaLicenseContract.View::class.java)
+ ) { _: Any?, _: Method?, _: Array? -> null } as MediaLicenseContract.View
+ }
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragment.java b/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragment.java
index 53ba6b75c9..884ad9831f 100644
--- a/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragment.java
+++ b/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragment.java
@@ -521,7 +521,7 @@ public void showDuplicatePicturePopup(UploadItem uploadItem) {
getString(R.string.duplicate_file_name),
String.format(Locale.getDefault(),
uploadTitleFormat,
- uploadItem.getFileName()),
+ uploadItem.getFilename()),
getString(R.string.upload),
getString(R.string.cancel),
() -> {
@@ -714,7 +714,7 @@ private void onEditActivityResult(ActivityResult result){
if (binding != null){
binding.backgroundImage.setImageURI(Uri.fromFile(new File(path)));
}
- editableUploadItem.setContentUri(Uri.fromFile(new File(path)));
+ editableUploadItem.setContentAndMediaUri(Uri.fromFile(new File(path)));
callback.changeThumbnail(indexOfFragment,
path);
} catch (Exception e) {
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaPresenter.java b/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaPresenter.java
index e626ee8761..35d281201f 100644
--- a/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaPresenter.java
+++ b/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaPresenter.java
@@ -106,7 +106,7 @@ public void onDetachView() {
*/
@Override
public void setUploadMediaDetails(final List uploadMediaDetails, final int uploadItemIndex) {
- repository.getUploads().get(uploadItemIndex).setMediaDetails(uploadMediaDetails);
+ repository.getUploads().get(uploadItemIndex).setUploadMediaDetails(uploadMediaDetails);
}
/**
@@ -284,7 +284,7 @@ public void handleCaptionResult(final Integer errorCode, final UploadItem upload
public void copyTitleAndDescriptionToSubsequentMedia(final int indexInViewFlipper) {
for(int i = indexInViewFlipper+1; i < repository.getCount(); i++){
final UploadItem subsequentUploadItem = repository.getUploads().get(i);
- subsequentUploadItem.setMediaDetails(deepCopy(repository.getUploads().get(indexInViewFlipper).getUploadMediaDetails()));
+ subsequentUploadItem.setUploadMediaDetails(deepCopy(repository.getUploads().get(indexInViewFlipper).getUploadMediaDetails()));
}
}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/structure/depictions/DepictModel.kt b/app/src/main/java/fr/free/nrw/commons/upload/structure/depictions/DepictModel.kt
index 9337cb8b58..6bbd5cfa51 100644
--- a/app/src/main/java/fr/free/nrw/commons/upload/structure/depictions/DepictModel.kt
+++ b/app/src/main/java/fr/free/nrw/commons/upload/structure/depictions/DepictModel.kt
@@ -40,11 +40,11 @@ class DepictModel
place.wikiDataEntityId?.let { qids.add(it) }
}
repository.getUploads().forEach { item ->
- if (item.gpsCoords != null && item.gpsCoords.imageCoordsExists) {
+ if (item.gpsCoords != null && item.gpsCoords?.imageCoordsExists == true) {
Coordinates2Country
.countryQID(
- item.gpsCoords.decLatitude,
- item.gpsCoords.decLongitude,
+ item.gpsCoords!!.decLatitude,
+ item.gpsCoords!!.decLongitude,
)?.let { qids.add("Q$it") }
}
}
diff --git a/app/src/test/kotlin/fr/free/nrw/commons/upload/CategoriesPresenterTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/upload/CategoriesPresenterTest.kt
index bb8fd1fc5e..536f61d20f 100644
--- a/app/src/test/kotlin/fr/free/nrw/commons/upload/CategoriesPresenterTest.kt
+++ b/app/src/test/kotlin/fr/free/nrw/commons/upload/CategoriesPresenterTest.kt
@@ -84,10 +84,10 @@ class CategoriesPresenterTest {
)
val nonEmptyCaptionUploadItem = mock()
whenever(nonEmptyCaptionUploadItem.uploadMediaDetails)
- .thenReturn(listOf(UploadMediaDetail(captionText = "nonEmpty")))
+ .thenReturn(mutableListOf(UploadMediaDetail(captionText = "nonEmpty")))
val emptyCaptionUploadItem = mock()
whenever(emptyCaptionUploadItem.uploadMediaDetails)
- .thenReturn(listOf(UploadMediaDetail(captionText = "")))
+ .thenReturn(mutableListOf(UploadMediaDetail(captionText = "")))
whenever(repository.getUploads()).thenReturn(
listOf(
nonEmptyCaptionUploadItem,
diff --git a/app/src/test/kotlin/fr/free/nrw/commons/upload/ImageProcessingServiceTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/upload/ImageProcessingServiceTest.kt
index 30f553e722..90d2f20e65 100644
--- a/app/src/test/kotlin/fr/free/nrw/commons/upload/ImageProcessingServiceTest.kt
+++ b/app/src/test/kotlin/fr/free/nrw/commons/upload/ImageProcessingServiceTest.kt
@@ -64,7 +64,7 @@ class ImageProcessingServiceTest {
`when`(uploadItem.uploadMediaDetails).thenReturn(mockTitle as MutableList?)
`when`(uploadItem.place).thenReturn(mockPlace)
- `when`(uploadItem.fileName).thenReturn("File:jpg")
+ `when`(uploadItem.filename).thenReturn("File:jpg")
`when`(fileUtilsWrapper!!.getFileInputStream(ArgumentMatchers.anyString()))
.thenReturn(mock(FileInputStream::class.java))
diff --git a/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadMediaPresenterTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadMediaPresenterTest.kt
index 47c4d0ae56..5793d6e79e 100644
--- a/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadMediaPresenterTest.kt
+++ b/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadMediaPresenterTest.kt
@@ -137,7 +137,7 @@ class UploadMediaPresenterTest {
whenever(uploadItem.imageQuality).thenReturn(0)
whenever(uploadItem.gpsCoords)
.thenReturn(imageCoordinates)
- whenever(uploadItem.gpsCoords.decimalCoords)
+ whenever(uploadItem.gpsCoords?.decimalCoords)
.thenReturn("imageCoordinates")
uploadMediaPresenter.getImageQuality(0, location, mockActivity)
verify(view).showProgress(true)
@@ -155,7 +155,7 @@ class UploadMediaPresenterTest {
whenever(uploadItem.imageQuality).thenReturn(0)
whenever(uploadItem.gpsCoords)
.thenReturn(imageCoordinates)
- whenever(uploadItem.gpsCoords.decimalCoords)
+ whenever(uploadItem.gpsCoords?.decimalCoords)
.thenReturn(null)
uploadMediaPresenter.getImageQuality(0, location, mockActivity)
testScheduler.triggerActions()
@@ -195,7 +195,7 @@ class UploadMediaPresenterTest {
uploadMediaDetail.languageCode = "en"
val uploadMediaDetailList: ArrayList = ArrayList()
uploadMediaDetailList.add(uploadMediaDetail)
- uploadItem.setMediaDetails(uploadMediaDetailList)
+ uploadItem.uploadMediaDetails = uploadMediaDetailList
Mockito.`when`(repository.getImageQuality(uploadItem, location)).then {
verify(view).showProgress(true)
testScheduler.triggerActions()
@@ -211,7 +211,7 @@ class UploadMediaPresenterTest {
uploadMediaDetail.languageCode = "en"
uploadMediaDetail.captionText = "added caption"
uploadMediaDetail.languageCode = "eo"
- uploadItem.setMediaDetails(Collections.singletonList(uploadMediaDetail))
+ uploadItem.uploadMediaDetails = Collections.singletonList(uploadMediaDetail)
Mockito.`when`(repository.getImageQuality(uploadItem, location)).then {
verify(view).showProgress(true)
testScheduler.triggerActions()
@@ -228,7 +228,7 @@ class UploadMediaPresenterTest {
whenever(repository.getUploads()).thenReturn(listOf(uploadItem))
whenever(repository.getUploadItem(ArgumentMatchers.anyInt()))
.thenReturn(uploadItem)
- whenever(uploadItem.uploadMediaDetails).thenReturn(listOf())
+ whenever(uploadItem.uploadMediaDetails).thenReturn(mutableListOf())
uploadMediaPresenter.fetchTitleAndDescription(0)
verify(view).updateMediaDetails(ArgumentMatchers.any())
diff --git a/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadPresenterTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadPresenterTest.kt
index 8741c2da87..1e06888746 100644
--- a/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadPresenterTest.kt
+++ b/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadPresenterTest.kt
@@ -83,7 +83,7 @@ class UploadPresenterTest {
@Test
fun handleSubmitImagesNoLocationWithConsecutiveNoLocationUploads() {
`when`(imageCoords.imageCoordsExists).thenReturn(false)
- `when`(uploadItem.getGpsCoords()).thenReturn(imageCoords)
+ `when`(uploadItem.gpsCoords).thenReturn(imageCoords)
`when`(repository.getUploads()).thenReturn(uploadableItems)
uploadableItems.add(uploadItem)
@@ -111,7 +111,7 @@ class UploadPresenterTest {
defaultKvStore.getInt(UploadPresenter.COUNTER_OF_CONSECUTIVE_UPLOADS_WITHOUT_COORDINATES, 0),
).thenReturn(UploadPresenter.CONSECUTIVE_UPLOADS_WITHOUT_COORDINATES_REMINDER_THRESHOLD)
`when`(imageCoords.imageCoordsExists).thenReturn(true)
- `when`(uploadItem.getGpsCoords()).thenReturn(imageCoords)
+ `when`(uploadItem.gpsCoords).thenReturn(imageCoords)
`when`(repository.getUploads()).thenReturn(uploadableItems)
uploadableItems.add(uploadItem)
uploadPresenter.handleSubmit()
diff --git a/app/src/test/kotlin/fr/free/nrw/commons/upload/categories/UploadCategoriesFragmentUnitTests.kt b/app/src/test/kotlin/fr/free/nrw/commons/upload/categories/UploadCategoriesFragmentUnitTests.kt
index f2d54132c1..75d6b8a4ff 100644
--- a/app/src/test/kotlin/fr/free/nrw/commons/upload/categories/UploadCategoriesFragmentUnitTests.kt
+++ b/app/src/test/kotlin/fr/free/nrw/commons/upload/categories/UploadCategoriesFragmentUnitTests.kt
@@ -62,6 +62,7 @@ class UploadCategoriesFragmentUnitTests {
OkHttpConnectionFactory.CLIENT = createTestClient()
val activity = Robolectric.buildActivity(UploadActivity::class.java).create().get()
fragment = UploadCategoriesFragment()
+ fragment.callback = callback
fragmentManager = activity.supportFragmentManager
val fragmentTransaction: FragmentTransaction = fragmentManager.beginTransaction()
fragmentTransaction.add(fragment, null)
diff --git a/app/src/test/kotlin/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragmentUnitTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragmentUnitTest.kt
index 029a8efdfd..cbd1f8ca73 100644
--- a/app/src/test/kotlin/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragmentUnitTest.kt
+++ b/app/src/test/kotlin/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragmentUnitTest.kt
@@ -481,7 +481,7 @@ class UploadMediaDetailFragmentUnitTest {
`when`(imageCoordinates.zoomLevel).thenReturn(14.0)
`when`(defaultKvStore.getString(LAST_ZOOM)).thenReturn(null)
fragment.showExternalMap(uploadItem)
- Mockito.verify(uploadItem.gpsCoords, Mockito.times(1)).zoomLevel
+ Mockito.verify(uploadItem.gpsCoords, Mockito.times(1))?.zoomLevel
val shadowActivity: ShadowActivity = shadowOf(activity)
val startedIntent = shadowActivity.nextStartedActivity
val shadowIntent: ShadowIntent = shadowOf(startedIntent)