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)