mirror of
https://github.com/commons-app/apps-android-commons.git
synced 2025-10-26 20:33:53 +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(
|
||||
formatCaptions(item.uploadMediaDetails),
|
||||
categories,
|
||||
item.fileName,
|
||||
item.filename,
|
||||
formatDescriptions(item.uploadMediaDetails),
|
||||
sessionManager.userName,
|
||||
sessionManager.userName,
|
||||
),
|
||||
localUri = item.mediaUri,
|
||||
decimalCoords = item.gpsCoords.decimalCoords,
|
||||
decimalCoords = item.gpsCoords?.decimalCoords,
|
||||
dateCreatedSource = "",
|
||||
depictedItems = depictedItems,
|
||||
wikidataPlace = from(item.place),
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ class MimeTypeMapWrapper {
|
|||
)
|
||||
|
||||
@JvmStatic
|
||||
fun getExtensionFromMimeType(mimeType: String): String? {
|
||||
fun getExtensionFromMimeType(mimeType: String?): String? {
|
||||
val result = sMimeTypeToExtensionMap[mimeType]
|
||||
if (result != null) {
|
||||
return result
|
||||
|
|
|
|||
|
|
@ -243,7 +243,7 @@ class UploadRepository @Inject constructor(
|
|||
*
|
||||
* @param licenseName
|
||||
*/
|
||||
fun setSelectedLicense(licenseName: String) {
|
||||
fun setSelectedLicense(licenseName: String?) {
|
||||
uploadModel.selectedLicense = licenseName
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ class ImageProcessingService @Inject constructor(
|
|||
}
|
||||
|
||||
Timber.d("Checking the validity of image")
|
||||
val filePath = uploadItem.mediaUri.path
|
||||
val filePath = uploadItem.mediaUri?.path
|
||||
|
||||
return Single.zip(
|
||||
checkDuplicateImage(filePath),
|
||||
|
|
@ -107,7 +107,7 @@ class ImageProcessingService @Inject constructor(
|
|||
return Single.just(EMPTY_CAPTION)
|
||||
}
|
||||
|
||||
return mediaClient.checkPageExistsUsingTitle("File:" + uploadItem.fileName)
|
||||
return mediaClient.checkPageExistsUsingTitle("File:" + uploadItem.filename)
|
||||
.map { doesFileExist: Boolean ->
|
||||
Timber.d("Result for valid title is %s", doesFileExist)
|
||||
if (doesFileExist) FILE_NAME_EXISTS else IMAGE_OK
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
ThumbnailsAdapter.context=this;
|
||||
final Intent intent = getIntent();
|
||||
final String action = intent.getAction();
|
||||
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 updateLicenseSummary(selectedLicense: String?, numberOfItems: Int?)
|
||||
fun updateLicenseSummary(selectedLicense: String?, numberOfItems: Int)
|
||||
}
|
||||
|
||||
interface UserActionListener : BasePresenter<View> {
|
||||
fun getLicenses()
|
||||
|
||||
fun selectLicense(licenseName: String)
|
||||
fun selectLicense(licenseName: String?)
|
||||
|
||||
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),
|
||||
String.format(Locale.getDefault(),
|
||||
uploadTitleFormat,
|
||||
uploadItem.getFileName()),
|
||||
uploadItem.getFilename()),
|
||||
getString(R.string.upload),
|
||||
getString(R.string.cancel),
|
||||
() -> {
|
||||
|
|
@ -714,7 +714,7 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
|
|||
if (binding != null){
|
||||
binding.backgroundImage.setImageURI(Uri.fromFile(new File(path)));
|
||||
}
|
||||
editableUploadItem.setContentUri(Uri.fromFile(new File(path)));
|
||||
editableUploadItem.setContentAndMediaUri(Uri.fromFile(new File(path)));
|
||||
callback.changeThumbnail(indexOfFragment,
|
||||
path);
|
||||
} catch (Exception e) {
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@ public class UploadMediaPresenter implements UserActionListener, SimilarImageInt
|
|||
*/
|
||||
@Override
|
||||
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) {
|
||||
for(int i = indexInViewFlipper+1; i < repository.getCount(); i++){
|
||||
final UploadItem subsequentUploadItem = repository.getUploads().get(i);
|
||||
subsequentUploadItem.setMediaDetails(deepCopy(repository.getUploads().get(indexInViewFlipper).getUploadMediaDetails()));
|
||||
subsequentUploadItem.setUploadMediaDetails(deepCopy(repository.getUploads().get(indexInViewFlipper).getUploadMediaDetails()));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -40,11 +40,11 @@ class DepictModel
|
|||
place.wikiDataEntityId?.let { qids.add(it) }
|
||||
}
|
||||
repository.getUploads().forEach { item ->
|
||||
if (item.gpsCoords != null && item.gpsCoords.imageCoordsExists) {
|
||||
if (item.gpsCoords != null && item.gpsCoords?.imageCoordsExists == true) {
|
||||
Coordinates2Country
|
||||
.countryQID(
|
||||
item.gpsCoords.decLatitude,
|
||||
item.gpsCoords.decLongitude,
|
||||
item.gpsCoords!!.decLatitude,
|
||||
item.gpsCoords!!.decLongitude,
|
||||
)?.let { qids.add("Q$it") }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -84,10 +84,10 @@ class CategoriesPresenterTest {
|
|||
)
|
||||
val nonEmptyCaptionUploadItem = mock<UploadItem>()
|
||||
whenever(nonEmptyCaptionUploadItem.uploadMediaDetails)
|
||||
.thenReturn(listOf(UploadMediaDetail(captionText = "nonEmpty")))
|
||||
.thenReturn(mutableListOf(UploadMediaDetail(captionText = "nonEmpty")))
|
||||
val emptyCaptionUploadItem = mock<UploadItem>()
|
||||
whenever(emptyCaptionUploadItem.uploadMediaDetails)
|
||||
.thenReturn(listOf(UploadMediaDetail(captionText = "")))
|
||||
.thenReturn(mutableListOf(UploadMediaDetail(captionText = "")))
|
||||
whenever(repository.getUploads()).thenReturn(
|
||||
listOf(
|
||||
nonEmptyCaptionUploadItem,
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ class ImageProcessingServiceTest {
|
|||
`when`(uploadItem.uploadMediaDetails).thenReturn(mockTitle as MutableList<UploadMediaDetail>?)
|
||||
|
||||
`when`(uploadItem.place).thenReturn(mockPlace)
|
||||
`when`(uploadItem.fileName).thenReturn("File:jpg")
|
||||
`when`(uploadItem.filename).thenReturn("File:jpg")
|
||||
|
||||
`when`(fileUtilsWrapper!!.getFileInputStream(ArgumentMatchers.anyString()))
|
||||
.thenReturn(mock(FileInputStream::class.java))
|
||||
|
|
|
|||
|
|
@ -137,7 +137,7 @@ class UploadMediaPresenterTest {
|
|||
whenever(uploadItem.imageQuality).thenReturn(0)
|
||||
whenever(uploadItem.gpsCoords)
|
||||
.thenReturn(imageCoordinates)
|
||||
whenever(uploadItem.gpsCoords.decimalCoords)
|
||||
whenever(uploadItem.gpsCoords?.decimalCoords)
|
||||
.thenReturn("imageCoordinates")
|
||||
uploadMediaPresenter.getImageQuality(0, location, mockActivity)
|
||||
verify(view).showProgress(true)
|
||||
|
|
@ -155,7 +155,7 @@ class UploadMediaPresenterTest {
|
|||
whenever(uploadItem.imageQuality).thenReturn(0)
|
||||
whenever(uploadItem.gpsCoords)
|
||||
.thenReturn(imageCoordinates)
|
||||
whenever(uploadItem.gpsCoords.decimalCoords)
|
||||
whenever(uploadItem.gpsCoords?.decimalCoords)
|
||||
.thenReturn(null)
|
||||
uploadMediaPresenter.getImageQuality(0, location, mockActivity)
|
||||
testScheduler.triggerActions()
|
||||
|
|
@ -195,7 +195,7 @@ class UploadMediaPresenterTest {
|
|||
uploadMediaDetail.languageCode = "en"
|
||||
val uploadMediaDetailList: ArrayList<UploadMediaDetail> = ArrayList()
|
||||
uploadMediaDetailList.add(uploadMediaDetail)
|
||||
uploadItem.setMediaDetails(uploadMediaDetailList)
|
||||
uploadItem.uploadMediaDetails = uploadMediaDetailList
|
||||
Mockito.`when`(repository.getImageQuality(uploadItem, location)).then {
|
||||
verify(view).showProgress(true)
|
||||
testScheduler.triggerActions()
|
||||
|
|
@ -211,7 +211,7 @@ class UploadMediaPresenterTest {
|
|||
uploadMediaDetail.languageCode = "en"
|
||||
uploadMediaDetail.captionText = "added caption"
|
||||
uploadMediaDetail.languageCode = "eo"
|
||||
uploadItem.setMediaDetails(Collections.singletonList(uploadMediaDetail))
|
||||
uploadItem.uploadMediaDetails = Collections.singletonList(uploadMediaDetail)
|
||||
Mockito.`when`(repository.getImageQuality(uploadItem, location)).then {
|
||||
verify(view).showProgress(true)
|
||||
testScheduler.triggerActions()
|
||||
|
|
@ -228,7 +228,7 @@ class UploadMediaPresenterTest {
|
|||
whenever(repository.getUploads()).thenReturn(listOf(uploadItem))
|
||||
whenever(repository.getUploadItem(ArgumentMatchers.anyInt()))
|
||||
.thenReturn(uploadItem)
|
||||
whenever(uploadItem.uploadMediaDetails).thenReturn(listOf())
|
||||
whenever(uploadItem.uploadMediaDetails).thenReturn(mutableListOf())
|
||||
|
||||
uploadMediaPresenter.fetchTitleAndDescription(0)
|
||||
verify(view).updateMediaDetails(ArgumentMatchers.any())
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ class UploadPresenterTest {
|
|||
@Test
|
||||
fun handleSubmitImagesNoLocationWithConsecutiveNoLocationUploads() {
|
||||
`when`(imageCoords.imageCoordsExists).thenReturn(false)
|
||||
`when`(uploadItem.getGpsCoords()).thenReturn(imageCoords)
|
||||
`when`(uploadItem.gpsCoords).thenReturn(imageCoords)
|
||||
`when`(repository.getUploads()).thenReturn(uploadableItems)
|
||||
uploadableItems.add(uploadItem)
|
||||
|
||||
|
|
@ -111,7 +111,7 @@ class UploadPresenterTest {
|
|||
defaultKvStore.getInt(UploadPresenter.COUNTER_OF_CONSECUTIVE_UPLOADS_WITHOUT_COORDINATES, 0),
|
||||
).thenReturn(UploadPresenter.CONSECUTIVE_UPLOADS_WITHOUT_COORDINATES_REMINDER_THRESHOLD)
|
||||
`when`(imageCoords.imageCoordsExists).thenReturn(true)
|
||||
`when`(uploadItem.getGpsCoords()).thenReturn(imageCoords)
|
||||
`when`(uploadItem.gpsCoords).thenReturn(imageCoords)
|
||||
`when`(repository.getUploads()).thenReturn(uploadableItems)
|
||||
uploadableItems.add(uploadItem)
|
||||
uploadPresenter.handleSubmit()
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ class UploadCategoriesFragmentUnitTests {
|
|||
OkHttpConnectionFactory.CLIENT = createTestClient()
|
||||
val activity = Robolectric.buildActivity(UploadActivity::class.java).create().get()
|
||||
fragment = UploadCategoriesFragment()
|
||||
fragment.callback = callback
|
||||
fragmentManager = activity.supportFragmentManager
|
||||
val fragmentTransaction: FragmentTransaction = fragmentManager.beginTransaction()
|
||||
fragmentTransaction.add(fragment, null)
|
||||
|
|
|
|||
|
|
@ -481,7 +481,7 @@ class UploadMediaDetailFragmentUnitTest {
|
|||
`when`(imageCoordinates.zoomLevel).thenReturn(14.0)
|
||||
`when`(defaultKvStore.getString(LAST_ZOOM)).thenReturn(null)
|
||||
fragment.showExternalMap(uploadItem)
|
||||
Mockito.verify(uploadItem.gpsCoords, Mockito.times(1)).zoomLevel
|
||||
Mockito.verify(uploadItem.gpsCoords, Mockito.times(1))?.zoomLevel
|
||||
val shadowActivity: ShadowActivity = shadowOf(activity)
|
||||
val startedIntent = shadowActivity.nextStartedActivity
|
||||
val shadowIntent: ShadowIntent = shadowOf(startedIntent)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue