mirror of
https://github.com/commons-app/apps-android-commons.git
synced 2025-10-27 04:43:54 +01:00
Convert upload to kotlin (part 2) (#6069)
* Convert UploadCategoriesFragment to kotlin * Convert DepictsFragment to kotlin * Convert MediaLicensePresenter to kotlin * Convert MediaLicenseFragment to kotlin * Converted SimilarImageDialogFragment to kotlin * Convert ThumbnailsAdapter to kotlin * Convert UploadPresenter to kotlin * Convert UploadBaseFragment to kotlin * Convert UploadMediaDetailInputFilter to kotlin * Convert UploadItem to kotlin * Convert UploadController to kotlin * Fix nullability of the UploadItem
This commit is contained in:
parent
369e79be5e
commit
a9058d129e
37 changed files with 1830 additions and 2100 deletions
|
|
@ -63,13 +63,13 @@ data class Contribution constructor(
|
||||||
Media(
|
Media(
|
||||||
formatCaptions(item.uploadMediaDetails),
|
formatCaptions(item.uploadMediaDetails),
|
||||||
categories,
|
categories,
|
||||||
item.fileName,
|
item.filename,
|
||||||
formatDescriptions(item.uploadMediaDetails),
|
formatDescriptions(item.uploadMediaDetails),
|
||||||
sessionManager.userName,
|
sessionManager.userName,
|
||||||
sessionManager.userName,
|
sessionManager.userName,
|
||||||
),
|
),
|
||||||
localUri = item.mediaUri,
|
localUri = item.mediaUri,
|
||||||
decimalCoords = item.gpsCoords.decimalCoords,
|
decimalCoords = item.gpsCoords?.decimalCoords,
|
||||||
dateCreatedSource = "",
|
dateCreatedSource = "",
|
||||||
depictedItems = depictedItems,
|
depictedItems = depictedItems,
|
||||||
wikidataPlace = from(item.place),
|
wikidataPlace = from(item.place),
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ class MimeTypeMapWrapper {
|
||||||
)
|
)
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun getExtensionFromMimeType(mimeType: String): String? {
|
fun getExtensionFromMimeType(mimeType: String?): String? {
|
||||||
val result = sMimeTypeToExtensionMap[mimeType]
|
val result = sMimeTypeToExtensionMap[mimeType]
|
||||||
if (result != null) {
|
if (result != null) {
|
||||||
return result
|
return result
|
||||||
|
|
|
||||||
|
|
@ -243,7 +243,7 @@ class UploadRepository @Inject constructor(
|
||||||
*
|
*
|
||||||
* @param licenseName
|
* @param licenseName
|
||||||
*/
|
*/
|
||||||
fun setSelectedLicense(licenseName: String) {
|
fun setSelectedLicense(licenseName: String?) {
|
||||||
uploadModel.selectedLicense = licenseName
|
uploadModel.selectedLicense = licenseName
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ class ImageProcessingService @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
Timber.d("Checking the validity of image")
|
Timber.d("Checking the validity of image")
|
||||||
val filePath = uploadItem.mediaUri.path
|
val filePath = uploadItem.mediaUri?.path
|
||||||
|
|
||||||
return Single.zip(
|
return Single.zip(
|
||||||
checkDuplicateImage(filePath),
|
checkDuplicateImage(filePath),
|
||||||
|
|
@ -107,7 +107,7 @@ class ImageProcessingService @Inject constructor(
|
||||||
return Single.just(EMPTY_CAPTION)
|
return Single.just(EMPTY_CAPTION)
|
||||||
}
|
}
|
||||||
|
|
||||||
return mediaClient.checkPageExistsUsingTitle("File:" + uploadItem.fileName)
|
return mediaClient.checkPageExistsUsingTitle("File:" + uploadItem.filename)
|
||||||
.map { doesFileExist: Boolean ->
|
.map { doesFileExist: Boolean ->
|
||||||
Timber.d("Result for valid title is %s", doesFileExist)
|
Timber.d("Result for valid title is %s", doesFileExist)
|
||||||
if (doesFileExist) FILE_NAME_EXISTS else IMAGE_OK
|
if (doesFileExist) FILE_NAME_EXISTS else IMAGE_OK
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<ThumbnailsAdapter.ViewHolder> {
|
|
||||||
public static Context context;
|
|
||||||
List<UploadableFile> 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<UploadableFile> 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -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<ThumbnailsAdapter.ViewHolder>() {
|
||||||
|
|
||||||
|
var onThumbnailDeletedListener: OnThumbnailDeletedListener? = null
|
||||||
|
var uploadableFiles: List<UploadableFile> = 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -448,7 +448,6 @@ public class UploadActivity extends BaseActivity implements
|
||||||
}
|
}
|
||||||
|
|
||||||
private void receiveSharedItems() {
|
private void receiveSharedItems() {
|
||||||
ThumbnailsAdapter.context=this;
|
|
||||||
final Intent intent = getIntent();
|
final Intent intent = getIntent();
|
||||||
final String action = intent.getAction();
|
final String action = intent.getAction();
|
||||||
if (Intent.ACTION_SEND.equals(action) || Intent.ACTION_SEND_MULTIPLE.equals(action)) {
|
if (Intent.ACTION_SEND.equals(action) || Intent.ACTION_SEND_MULTIPLE.equals(action)) {
|
||||||
|
|
|
||||||
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
166
app/src/main/java/fr/free/nrw/commons/upload/UploadController.kt
Normal file
166
app/src/main/java/fr/free/nrw/commons/upload/UploadController.kt
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<UploadMediaDetail> uploadMediaDetails;
|
|
||||||
private Place place;
|
|
||||||
private final long createdTimestamp;
|
|
||||||
private final String createdTimestampSource;
|
|
||||||
private final BehaviorSubject<Integer> 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<UploadMediaDetail> 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<UploadMediaDetail> 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
65
app/src/main/java/fr/free/nrw/commons/upload/UploadItem.kt
Normal file
65
app/src/main/java/fr/free/nrw/commons/upload/UploadItem.kt
Normal file
|
|
@ -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<UploadMediaDetail> = 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 <a href="https://commons.wikimedia.org/wiki/MediaWiki:Titleblacklist"></a>wikimedia.org</a>
|
|
||||||
*/
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<Contribution>() {
|
|
||||||
@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<UploadableFile> 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();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
192
app/src/main/java/fr/free/nrw/commons/upload/UploadPresenter.kt
Normal file
192
app/src/main/java/fr/free/nrw/commons/upload/UploadPresenter.kt
Normal file
|
|
@ -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<Contribution> {
|
||||||
|
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<Class<*>>(UploadContract.View::class.java)
|
||||||
|
) { _: Any?, _: Method?, _: Array<Any?>? -> 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<CategoryItem> 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<String> 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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<CategoryItem>? ->
|
||||||
|
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<CharSequence?>? ->
|
||||||
|
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<CategoryItem>?) {
|
||||||
|
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<String>? {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<DepictedItem> 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<String> 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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<DepictedItem>) {
|
||||||
|
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<String>? {
|
||||||
|
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<CharSequence?>? ->
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -11,13 +11,13 @@ interface MediaLicenseContract {
|
||||||
|
|
||||||
fun setSelectedLicense(license: String?)
|
fun setSelectedLicense(license: String?)
|
||||||
|
|
||||||
fun updateLicenseSummary(selectedLicense: String?, numberOfItems: Int?)
|
fun updateLicenseSummary(selectedLicense: String?, numberOfItems: Int)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UserActionListener : BasePresenter<View> {
|
interface UserActionListener : BasePresenter<View> {
|
||||||
fun getLicenses()
|
fun getLicenses()
|
||||||
|
|
||||||
fun selectLicense(licenseName: String)
|
fun selectLicense(licenseName: String?)
|
||||||
|
|
||||||
fun isWLMSupportedForThisPlace(): Boolean
|
fun isWLMSupportedForThisPlace(): Boolean
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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<String> adapter;
|
|
||||||
private List<String> 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<String> 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 = "<a href='" + Utils.licenseUrlFor(licenseSummary) + "'>" +
|
|
||||||
getString(Utils.licenseNameFor(licenseSummary)) + "</a><br>";
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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<String>? = null
|
||||||
|
private var licenses: List<String>? = 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<String>?) {
|
||||||
|
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 = "<a href='" + Utils.licenseUrlFor(selectedLicense) + "'>" +
|
||||||
|
getString(Utils.licenseNameFor(selectedLicense)) + "</a><br>"
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<String> 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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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<Class<*>>(MediaLicenseContract.View::class.java)
|
||||||
|
) { _: Any?, _: Method?, _: Array<Any?>? -> null } as MediaLicenseContract.View
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -521,7 +521,7 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
|
||||||
getString(R.string.duplicate_file_name),
|
getString(R.string.duplicate_file_name),
|
||||||
String.format(Locale.getDefault(),
|
String.format(Locale.getDefault(),
|
||||||
uploadTitleFormat,
|
uploadTitleFormat,
|
||||||
uploadItem.getFileName()),
|
uploadItem.getFilename()),
|
||||||
getString(R.string.upload),
|
getString(R.string.upload),
|
||||||
getString(R.string.cancel),
|
getString(R.string.cancel),
|
||||||
() -> {
|
() -> {
|
||||||
|
|
@ -714,7 +714,7 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
|
||||||
if (binding != null){
|
if (binding != null){
|
||||||
binding.backgroundImage.setImageURI(Uri.fromFile(new File(path)));
|
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,
|
callback.changeThumbnail(indexOfFragment,
|
||||||
path);
|
path);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
|
|
||||||
|
|
@ -106,7 +106,7 @@ public class UploadMediaPresenter implements UserActionListener, SimilarImageInt
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void setUploadMediaDetails(final List<UploadMediaDetail> uploadMediaDetails, final int uploadItemIndex) {
|
public void setUploadMediaDetails(final List<UploadMediaDetail> uploadMediaDetails, final int uploadItemIndex) {
|
||||||
repository.getUploads().get(uploadItemIndex).setMediaDetails(uploadMediaDetails);
|
repository.getUploads().get(uploadItemIndex).setUploadMediaDetails(uploadMediaDetails);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -284,7 +284,7 @@ public class UploadMediaPresenter implements UserActionListener, SimilarImageInt
|
||||||
public void copyTitleAndDescriptionToSubsequentMedia(final int indexInViewFlipper) {
|
public void copyTitleAndDescriptionToSubsequentMedia(final int indexInViewFlipper) {
|
||||||
for(int i = indexInViewFlipper+1; i < repository.getCount(); i++){
|
for(int i = indexInViewFlipper+1; i < repository.getCount(); i++){
|
||||||
final UploadItem subsequentUploadItem = repository.getUploads().get(i);
|
final UploadItem subsequentUploadItem = repository.getUploads().get(i);
|
||||||
subsequentUploadItem.setMediaDetails(deepCopy(repository.getUploads().get(indexInViewFlipper).getUploadMediaDetails()));
|
subsequentUploadItem.setUploadMediaDetails(deepCopy(repository.getUploads().get(indexInViewFlipper).getUploadMediaDetails()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -40,11 +40,11 @@ class DepictModel
|
||||||
place.wikiDataEntityId?.let { qids.add(it) }
|
place.wikiDataEntityId?.let { qids.add(it) }
|
||||||
}
|
}
|
||||||
repository.getUploads().forEach { item ->
|
repository.getUploads().forEach { item ->
|
||||||
if (item.gpsCoords != null && item.gpsCoords.imageCoordsExists) {
|
if (item.gpsCoords != null && item.gpsCoords?.imageCoordsExists == true) {
|
||||||
Coordinates2Country
|
Coordinates2Country
|
||||||
.countryQID(
|
.countryQID(
|
||||||
item.gpsCoords.decLatitude,
|
item.gpsCoords!!.decLatitude,
|
||||||
item.gpsCoords.decLongitude,
|
item.gpsCoords!!.decLongitude,
|
||||||
)?.let { qids.add("Q$it") }
|
)?.let { qids.add("Q$it") }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -84,10 +84,10 @@ class CategoriesPresenterTest {
|
||||||
)
|
)
|
||||||
val nonEmptyCaptionUploadItem = mock<UploadItem>()
|
val nonEmptyCaptionUploadItem = mock<UploadItem>()
|
||||||
whenever(nonEmptyCaptionUploadItem.uploadMediaDetails)
|
whenever(nonEmptyCaptionUploadItem.uploadMediaDetails)
|
||||||
.thenReturn(listOf(UploadMediaDetail(captionText = "nonEmpty")))
|
.thenReturn(mutableListOf(UploadMediaDetail(captionText = "nonEmpty")))
|
||||||
val emptyCaptionUploadItem = mock<UploadItem>()
|
val emptyCaptionUploadItem = mock<UploadItem>()
|
||||||
whenever(emptyCaptionUploadItem.uploadMediaDetails)
|
whenever(emptyCaptionUploadItem.uploadMediaDetails)
|
||||||
.thenReturn(listOf(UploadMediaDetail(captionText = "")))
|
.thenReturn(mutableListOf(UploadMediaDetail(captionText = "")))
|
||||||
whenever(repository.getUploads()).thenReturn(
|
whenever(repository.getUploads()).thenReturn(
|
||||||
listOf(
|
listOf(
|
||||||
nonEmptyCaptionUploadItem,
|
nonEmptyCaptionUploadItem,
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,7 @@ class ImageProcessingServiceTest {
|
||||||
`when`(uploadItem.uploadMediaDetails).thenReturn(mockTitle as MutableList<UploadMediaDetail>?)
|
`when`(uploadItem.uploadMediaDetails).thenReturn(mockTitle as MutableList<UploadMediaDetail>?)
|
||||||
|
|
||||||
`when`(uploadItem.place).thenReturn(mockPlace)
|
`when`(uploadItem.place).thenReturn(mockPlace)
|
||||||
`when`(uploadItem.fileName).thenReturn("File:jpg")
|
`when`(uploadItem.filename).thenReturn("File:jpg")
|
||||||
|
|
||||||
`when`(fileUtilsWrapper!!.getFileInputStream(ArgumentMatchers.anyString()))
|
`when`(fileUtilsWrapper!!.getFileInputStream(ArgumentMatchers.anyString()))
|
||||||
.thenReturn(mock(FileInputStream::class.java))
|
.thenReturn(mock(FileInputStream::class.java))
|
||||||
|
|
|
||||||
|
|
@ -137,7 +137,7 @@ class UploadMediaPresenterTest {
|
||||||
whenever(uploadItem.imageQuality).thenReturn(0)
|
whenever(uploadItem.imageQuality).thenReturn(0)
|
||||||
whenever(uploadItem.gpsCoords)
|
whenever(uploadItem.gpsCoords)
|
||||||
.thenReturn(imageCoordinates)
|
.thenReturn(imageCoordinates)
|
||||||
whenever(uploadItem.gpsCoords.decimalCoords)
|
whenever(uploadItem.gpsCoords?.decimalCoords)
|
||||||
.thenReturn("imageCoordinates")
|
.thenReturn("imageCoordinates")
|
||||||
uploadMediaPresenter.getImageQuality(0, location, mockActivity)
|
uploadMediaPresenter.getImageQuality(0, location, mockActivity)
|
||||||
verify(view).showProgress(true)
|
verify(view).showProgress(true)
|
||||||
|
|
@ -155,7 +155,7 @@ class UploadMediaPresenterTest {
|
||||||
whenever(uploadItem.imageQuality).thenReturn(0)
|
whenever(uploadItem.imageQuality).thenReturn(0)
|
||||||
whenever(uploadItem.gpsCoords)
|
whenever(uploadItem.gpsCoords)
|
||||||
.thenReturn(imageCoordinates)
|
.thenReturn(imageCoordinates)
|
||||||
whenever(uploadItem.gpsCoords.decimalCoords)
|
whenever(uploadItem.gpsCoords?.decimalCoords)
|
||||||
.thenReturn(null)
|
.thenReturn(null)
|
||||||
uploadMediaPresenter.getImageQuality(0, location, mockActivity)
|
uploadMediaPresenter.getImageQuality(0, location, mockActivity)
|
||||||
testScheduler.triggerActions()
|
testScheduler.triggerActions()
|
||||||
|
|
@ -195,7 +195,7 @@ class UploadMediaPresenterTest {
|
||||||
uploadMediaDetail.languageCode = "en"
|
uploadMediaDetail.languageCode = "en"
|
||||||
val uploadMediaDetailList: ArrayList<UploadMediaDetail> = ArrayList()
|
val uploadMediaDetailList: ArrayList<UploadMediaDetail> = ArrayList()
|
||||||
uploadMediaDetailList.add(uploadMediaDetail)
|
uploadMediaDetailList.add(uploadMediaDetail)
|
||||||
uploadItem.setMediaDetails(uploadMediaDetailList)
|
uploadItem.uploadMediaDetails = uploadMediaDetailList
|
||||||
Mockito.`when`(repository.getImageQuality(uploadItem, location)).then {
|
Mockito.`when`(repository.getImageQuality(uploadItem, location)).then {
|
||||||
verify(view).showProgress(true)
|
verify(view).showProgress(true)
|
||||||
testScheduler.triggerActions()
|
testScheduler.triggerActions()
|
||||||
|
|
@ -211,7 +211,7 @@ class UploadMediaPresenterTest {
|
||||||
uploadMediaDetail.languageCode = "en"
|
uploadMediaDetail.languageCode = "en"
|
||||||
uploadMediaDetail.captionText = "added caption"
|
uploadMediaDetail.captionText = "added caption"
|
||||||
uploadMediaDetail.languageCode = "eo"
|
uploadMediaDetail.languageCode = "eo"
|
||||||
uploadItem.setMediaDetails(Collections.singletonList(uploadMediaDetail))
|
uploadItem.uploadMediaDetails = Collections.singletonList(uploadMediaDetail)
|
||||||
Mockito.`when`(repository.getImageQuality(uploadItem, location)).then {
|
Mockito.`when`(repository.getImageQuality(uploadItem, location)).then {
|
||||||
verify(view).showProgress(true)
|
verify(view).showProgress(true)
|
||||||
testScheduler.triggerActions()
|
testScheduler.triggerActions()
|
||||||
|
|
@ -228,7 +228,7 @@ class UploadMediaPresenterTest {
|
||||||
whenever(repository.getUploads()).thenReturn(listOf(uploadItem))
|
whenever(repository.getUploads()).thenReturn(listOf(uploadItem))
|
||||||
whenever(repository.getUploadItem(ArgumentMatchers.anyInt()))
|
whenever(repository.getUploadItem(ArgumentMatchers.anyInt()))
|
||||||
.thenReturn(uploadItem)
|
.thenReturn(uploadItem)
|
||||||
whenever(uploadItem.uploadMediaDetails).thenReturn(listOf())
|
whenever(uploadItem.uploadMediaDetails).thenReturn(mutableListOf())
|
||||||
|
|
||||||
uploadMediaPresenter.fetchTitleAndDescription(0)
|
uploadMediaPresenter.fetchTitleAndDescription(0)
|
||||||
verify(view).updateMediaDetails(ArgumentMatchers.any())
|
verify(view).updateMediaDetails(ArgumentMatchers.any())
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,7 @@ class UploadPresenterTest {
|
||||||
@Test
|
@Test
|
||||||
fun handleSubmitImagesNoLocationWithConsecutiveNoLocationUploads() {
|
fun handleSubmitImagesNoLocationWithConsecutiveNoLocationUploads() {
|
||||||
`when`(imageCoords.imageCoordsExists).thenReturn(false)
|
`when`(imageCoords.imageCoordsExists).thenReturn(false)
|
||||||
`when`(uploadItem.getGpsCoords()).thenReturn(imageCoords)
|
`when`(uploadItem.gpsCoords).thenReturn(imageCoords)
|
||||||
`when`(repository.getUploads()).thenReturn(uploadableItems)
|
`when`(repository.getUploads()).thenReturn(uploadableItems)
|
||||||
uploadableItems.add(uploadItem)
|
uploadableItems.add(uploadItem)
|
||||||
|
|
||||||
|
|
@ -111,7 +111,7 @@ class UploadPresenterTest {
|
||||||
defaultKvStore.getInt(UploadPresenter.COUNTER_OF_CONSECUTIVE_UPLOADS_WITHOUT_COORDINATES, 0),
|
defaultKvStore.getInt(UploadPresenter.COUNTER_OF_CONSECUTIVE_UPLOADS_WITHOUT_COORDINATES, 0),
|
||||||
).thenReturn(UploadPresenter.CONSECUTIVE_UPLOADS_WITHOUT_COORDINATES_REMINDER_THRESHOLD)
|
).thenReturn(UploadPresenter.CONSECUTIVE_UPLOADS_WITHOUT_COORDINATES_REMINDER_THRESHOLD)
|
||||||
`when`(imageCoords.imageCoordsExists).thenReturn(true)
|
`when`(imageCoords.imageCoordsExists).thenReturn(true)
|
||||||
`when`(uploadItem.getGpsCoords()).thenReturn(imageCoords)
|
`when`(uploadItem.gpsCoords).thenReturn(imageCoords)
|
||||||
`when`(repository.getUploads()).thenReturn(uploadableItems)
|
`when`(repository.getUploads()).thenReturn(uploadableItems)
|
||||||
uploadableItems.add(uploadItem)
|
uploadableItems.add(uploadItem)
|
||||||
uploadPresenter.handleSubmit()
|
uploadPresenter.handleSubmit()
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,7 @@ class UploadCategoriesFragmentUnitTests {
|
||||||
OkHttpConnectionFactory.CLIENT = createTestClient()
|
OkHttpConnectionFactory.CLIENT = createTestClient()
|
||||||
val activity = Robolectric.buildActivity(UploadActivity::class.java).create().get()
|
val activity = Robolectric.buildActivity(UploadActivity::class.java).create().get()
|
||||||
fragment = UploadCategoriesFragment()
|
fragment = UploadCategoriesFragment()
|
||||||
|
fragment.callback = callback
|
||||||
fragmentManager = activity.supportFragmentManager
|
fragmentManager = activity.supportFragmentManager
|
||||||
val fragmentTransaction: FragmentTransaction = fragmentManager.beginTransaction()
|
val fragmentTransaction: FragmentTransaction = fragmentManager.beginTransaction()
|
||||||
fragmentTransaction.add(fragment, null)
|
fragmentTransaction.add(fragment, null)
|
||||||
|
|
|
||||||
|
|
@ -481,7 +481,7 @@ class UploadMediaDetailFragmentUnitTest {
|
||||||
`when`(imageCoordinates.zoomLevel).thenReturn(14.0)
|
`when`(imageCoordinates.zoomLevel).thenReturn(14.0)
|
||||||
`when`(defaultKvStore.getString(LAST_ZOOM)).thenReturn(null)
|
`when`(defaultKvStore.getString(LAST_ZOOM)).thenReturn(null)
|
||||||
fragment.showExternalMap(uploadItem)
|
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 shadowActivity: ShadowActivity = shadowOf(activity)
|
||||||
val startedIntent = shadowActivity.nextStartedActivity
|
val startedIntent = shadowActivity.nextStartedActivity
|
||||||
val shadowIntent: ShadowIntent = shadowOf(startedIntent)
|
val shadowIntent: ShadowIntent = shadowOf(startedIntent)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue