diff --git a/app/src/main/java/fr/free/nrw/commons/description/DescriptionEditActivity.kt b/app/src/main/java/fr/free/nrw/commons/description/DescriptionEditActivity.kt index 412a8219c..942946a6b 100644 --- a/app/src/main/java/fr/free/nrw/commons/description/DescriptionEditActivity.kt +++ b/app/src/main/java/fr/free/nrw/commons/description/DescriptionEditActivity.kt @@ -1,7 +1,6 @@ package fr.free.nrw.commons.description import android.app.ProgressDialog -import android.content.Intent import android.os.Bundle import android.os.Parcelable import android.speech.RecognizerIntent @@ -72,7 +71,7 @@ class DescriptionEditActivity : private lateinit var binding: ActivityDescriptionEditBinding - private var descriptionAndCaptions: ArrayList? = null + private var descriptionAndCaptions: MutableList? = null private val voiceInputResultLauncher = registerForActivityResult( ActivityResultContracts.StartActivityForResult() @@ -114,22 +113,18 @@ class DescriptionEditActivity : * Initializes the RecyclerView * @param descriptionAndCaptions list of description and caption */ - private fun initRecyclerView(descriptionAndCaptions: ArrayList?) { + private fun initRecyclerView(descriptionAndCaptions: MutableList?) { uploadMediaDetailAdapter = UploadMediaDetailAdapter( this, savedLanguageValue, - descriptionAndCaptions, + descriptionAndCaptions ?: mutableListOf(), recentLanguagesDao, voiceInputResultLauncher ) - uploadMediaDetailAdapter.setCallback { titleStringID: Int, messageStringId: Int -> - showInfoAlert( - titleStringID, - messageStringId, - ) - } - uploadMediaDetailAdapter.setEventListener(this) + + uploadMediaDetailAdapter.callback = UploadMediaDetailAdapter.Callback(::showInfoAlert) + uploadMediaDetailAdapter.eventListener = this rvDescriptions = binding.rvDescriptionsCaptions rvDescriptions!!.layoutManager = LinearLayoutManager(this) rvDescriptions!!.adapter = uploadMediaDetailAdapter diff --git a/app/src/main/java/fr/free/nrw/commons/recentlanguages/RecentLanguagesAdapter.kt b/app/src/main/java/fr/free/nrw/commons/recentlanguages/RecentLanguagesAdapter.kt index 81ef5533d..42eae100a 100644 --- a/app/src/main/java/fr/free/nrw/commons/recentlanguages/RecentLanguagesAdapter.kt +++ b/app/src/main/java/fr/free/nrw/commons/recentlanguages/RecentLanguagesAdapter.kt @@ -17,7 +17,7 @@ import java.util.HashMap class RecentLanguagesAdapter constructor( context: Context, var recentLanguages: List, - private val selectedLanguages: HashMap<*, String>, + private val selectedLanguages: MutableMap, ) : ArrayAdapter(context, R.layout.row_item_languages_spinner) { /** * Selected language code in UploadMediaDetailAdapter diff --git a/app/src/main/java/fr/free/nrw/commons/upload/LanguagesAdapter.kt b/app/src/main/java/fr/free/nrw/commons/upload/LanguagesAdapter.kt index fa825d0a6..a0d22009a 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/LanguagesAdapter.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/LanguagesAdapter.kt @@ -23,7 +23,7 @@ import java.util.Locale */ class LanguagesAdapter constructor( context: Context, - private val selectedLanguages: HashMap<*, String>, + private val selectedLanguages: MutableMap, ) : ArrayAdapter(context, R.layout.row_item_languages_spinner) { companion object { /** diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadMediaDetailAdapter.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadMediaDetailAdapter.java deleted file mode 100644 index de37e6855..000000000 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadMediaDetailAdapter.java +++ /dev/null @@ -1,633 +0,0 @@ -package fr.free.nrw.commons.upload; - -import android.app.Activity; -import android.app.Dialog; -import android.content.Intent; -import android.speech.RecognizerIntent; -import android.text.Editable; -import android.text.InputFilter; -import android.text.TextUtils; -import android.text.TextWatcher; -import android.view.LayoutInflater; -import android.view.View; -import android.view.View.OnClickListener; -import android.view.ViewGroup; -import android.widget.AdapterView; -import android.widget.AdapterView.OnItemClickListener; -import android.widget.Button; -import android.widget.EditText; -import android.widget.FrameLayout; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.ListView; -import android.widget.TextView; -import androidx.activity.result.ActivityResultLauncher; -import androidx.annotation.NonNull; -import androidx.constraintlayout.widget.ConstraintLayout; -import androidx.fragment.app.Fragment; -import androidx.recyclerview.widget.RecyclerView; -import com.google.android.material.textfield.TextInputLayout; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.databinding.RowItemDescriptionBinding; -import fr.free.nrw.commons.recentlanguages.Language; -import fr.free.nrw.commons.recentlanguages.RecentLanguagesAdapter; -import fr.free.nrw.commons.recentlanguages.RecentLanguagesDao; -import fr.free.nrw.commons.ui.PasteSensitiveTextInputEditText; -import fr.free.nrw.commons.utils.AbstractTextWatcher; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Objects; -import java.util.regex.Pattern; -import timber.log.Timber; - -public class UploadMediaDetailAdapter extends - RecyclerView.Adapter { - - RecentLanguagesDao recentLanguagesDao; - - private List uploadMediaDetails; - private Callback callback; - private EventListener eventListener; - - private HashMap selectedLanguages; - private final String savedLanguageValue; - private TextView recentLanguagesTextView; - private View separator; - private ListView languageHistoryListView; - private int currentPosition; - private Fragment fragment; - private Activity activity; - private final ActivityResultLauncher voiceInputResultLauncher; - private SelectedVoiceIcon selectedVoiceIcon; - - private RowItemDescriptionBinding binding; - - public UploadMediaDetailAdapter(Fragment fragment, String savedLanguageValue, - RecentLanguagesDao recentLanguagesDao, ActivityResultLauncher voiceInputResultLauncher) { - uploadMediaDetails = new ArrayList<>(); - selectedLanguages = new HashMap<>(); - this.savedLanguageValue = savedLanguageValue; - this.recentLanguagesDao = recentLanguagesDao; - this.fragment = fragment; - this.voiceInputResultLauncher = voiceInputResultLauncher; - } - - public UploadMediaDetailAdapter(Activity activity, final String savedLanguageValue, - List uploadMediaDetails, RecentLanguagesDao recentLanguagesDao, ActivityResultLauncher voiceInputResultLauncher) { - this.uploadMediaDetails = uploadMediaDetails; - selectedLanguages = new HashMap<>(); - this.savedLanguageValue = savedLanguageValue; - this.recentLanguagesDao = recentLanguagesDao; - this.activity = activity; - this.voiceInputResultLauncher = voiceInputResultLauncher; - } - - public void setCallback(Callback callback) { - this.callback = callback; - } - - public void setEventListener(EventListener eventListener) { - this.eventListener = eventListener; - } - - public void setItems(List uploadMediaDetails) { - this.uploadMediaDetails = uploadMediaDetails; - selectedLanguages = new HashMap<>(); - notifyDataSetChanged(); - } - - public List getItems() { - return uploadMediaDetails; - } - - @NonNull - @Override - public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - LayoutInflater inflater = LayoutInflater.from(parent.getContext()); - binding = RowItemDescriptionBinding.inflate(inflater, parent, false); - return new ViewHolder(binding.getRoot()); - } - - /** - * This is a workaround for a known bug by android here - * https://issuetracker.google.com/issues/37095917 makes the edit text on second and subsequent - * fragments inside an adapter receptive to long click for copy/paste options - * - * @param holder the view holder - */ - @Override - public void onViewAttachedToWindow(@NonNull final ViewHolder holder) { - super.onViewAttachedToWindow(holder); - holder.captionItemEditText.setEnabled(false); - holder.captionItemEditText.setEnabled(true); - holder.descItemEditText.setEnabled(false); - holder.descItemEditText.setEnabled(true); - } - - @Override - public void onBindViewHolder(@NonNull ViewHolder holder, int position) { - holder.bind(position); - } - - @Override - public int getItemCount() { - return uploadMediaDetails.size(); - } - - public void addDescription(UploadMediaDetail uploadMediaDetail) { - selectedLanguages.put(uploadMediaDetails.size(), "en"); - this.uploadMediaDetails.add(uploadMediaDetail); - notifyItemInserted(uploadMediaDetails.size()); - } - - private void startSpeechInput(String locale) { - Intent intent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH); - intent.putExtra( - RecognizerIntent.EXTRA_LANGUAGE_MODEL, - RecognizerIntent.LANGUAGE_MODEL_FREE_FORM - ); - intent.putExtra( - RecognizerIntent.EXTRA_LANGUAGE, - locale - ); - - try { - voiceInputResultLauncher.launch(intent); - } catch (Exception e) { - Timber.e(e.getMessage()); - } - } - - /** - * Handles the result of the speech input by processing the spoken text. - * If the spoken text is not empty, it capitalizes the first letter of the spoken text - * and updates the appropriate field (caption or description) of the current - * UploadMediaDetail based on the selected voice icon. - * Finally, it notifies the adapter that the data set has changed. - * - * @param spokenText the text input received from speech recognition. - */ - public void handleSpeechResult(String spokenText) { - if (!spokenText.isEmpty()) { - String spokenTextCapitalized = - spokenText.substring(0, 1).toUpperCase() + spokenText.substring(1); - if (currentPosition < uploadMediaDetails.size()) { - UploadMediaDetail uploadMediaDetail = uploadMediaDetails.get(currentPosition); - switch (selectedVoiceIcon) { - case CAPTION: - uploadMediaDetail.setCaptionText(spokenTextCapitalized); - break; - case DESCRIPTION: - uploadMediaDetail.setDescriptionText(spokenTextCapitalized); - break; - } - notifyDataSetChanged(); - } - } - } - - /** - * Remove description based on position from the list and notifies the RecyclerView Adapter that - * data in adapter has been removed at that particular position. - * - * @param uploadMediaDetail - * @param position - */ - public void removeDescription(final UploadMediaDetail uploadMediaDetail, final int position) { - selectedLanguages.remove(position); - this.uploadMediaDetails.remove(uploadMediaDetail); - int i = position + 1; - while (selectedLanguages.containsKey(i)) { - selectedLanguages.remove(i); - i++; - } - notifyItemRemoved(position); - notifyItemRangeChanged(position, uploadMediaDetails.size() - position); - updateAddButtonVisibility(); - } - - public class ViewHolder extends RecyclerView.ViewHolder { - - TextView descriptionLanguages ; - - PasteSensitiveTextInputEditText descItemEditText; - - TextInputLayout descInputLayout; - - PasteSensitiveTextInputEditText captionItemEditText; - - TextInputLayout captionInputLayout; - - ImageView removeButton; - - ImageView addButton; - - ConstraintLayout clParent; - - LinearLayout betterCaptionLinearLayout; - - LinearLayout betterDescriptionLinearLayout; - - private - - AbstractTextWatcher captionListener; - - AbstractTextWatcher descriptionListener; - - public ViewHolder(View itemView) { - super(itemView); - Timber.i("descItemEditText:" + descItemEditText); - } - - public void bind(int position) { - UploadMediaDetail uploadMediaDetail = uploadMediaDetails.get(position); - Timber.d("UploadMediaDetail is " + uploadMediaDetail); - - descriptionLanguages = binding.descriptionLanguages; - descItemEditText = binding.descriptionItemEditText; - descInputLayout = binding.descriptionItemEditTextInputLayout; - captionItemEditText = binding.captionItemEditText; - captionInputLayout = binding.captionItemEditTextInputLayout; - removeButton = binding.btnRemove; - addButton = binding.btnAdd; - clParent = binding.clParent; - betterCaptionLinearLayout = binding.llWriteBetterCaption; - betterDescriptionLinearLayout = binding.llWriteBetterDescription; - - - descriptionLanguages.setFocusable(false); - captionItemEditText.addTextChangedListener(new AbstractTextWatcher( - value -> { - if (position == 0) { - eventListener.onPrimaryCaptionTextChange(value.length() != 0); - } - })); - captionItemEditText.removeTextChangedListener(captionListener); - descItemEditText.removeTextChangedListener(descriptionListener); - captionItemEditText.setText(uploadMediaDetail.getCaptionText()); - descItemEditText.setText(uploadMediaDetail.getDescriptionText()); - captionInputLayout.setEndIconMode(TextInputLayout.END_ICON_CUSTOM); - captionInputLayout.setEndIconDrawable(R.drawable.baseline_keyboard_voice); - captionInputLayout.setEndIconOnClickListener(v -> { - currentPosition = position; - selectedVoiceIcon = SelectedVoiceIcon.CAPTION; - startSpeechInput(descriptionLanguages.getText().toString()); - }); - descInputLayout.setEndIconMode(TextInputLayout.END_ICON_CUSTOM); - descInputLayout.setEndIconDrawable(R.drawable.baseline_keyboard_voice); - descInputLayout.setEndIconOnClickListener(v -> { - currentPosition = position; - selectedVoiceIcon = SelectedVoiceIcon.DESCRIPTION; - startSpeechInput(descriptionLanguages.getText().toString()); - }); - - if (position == 0) { - removeButton.setVisibility(View.GONE); - betterCaptionLinearLayout.setVisibility(View.VISIBLE); - betterCaptionLinearLayout.setOnClickListener( - v -> callback.showAlert(R.string.media_detail_caption, R.string.caption_info)); - betterDescriptionLinearLayout.setVisibility(View.VISIBLE); - betterDescriptionLinearLayout.setOnClickListener( - v -> callback.showAlert(R.string.media_detail_description, - R.string.description_info)); - Objects.requireNonNull(captionInputLayout.getEditText()) - .setFilters(new InputFilter[]{ - new UploadMediaDetailInputFilter() - }); - } else { - removeButton.setVisibility(View.VISIBLE); - betterCaptionLinearLayout.setVisibility(View.GONE); - betterDescriptionLinearLayout.setVisibility(View.GONE); - } - - removeButton.setOnClickListener(v -> removeDescription(uploadMediaDetail, position)); - captionListener = new AbstractTextWatcher( - captionText -> uploadMediaDetail.setCaptionText( - convertIdeographicSpaceToLatinSpace(captionText.strip())) - ); - descriptionListener = new AbstractTextWatcher( - descriptionText -> uploadMediaDetail.setDescriptionText(descriptionText)); - captionItemEditText.addTextChangedListener(captionListener); - initLanguage(position, uploadMediaDetail); - - descItemEditText.addTextChangedListener(descriptionListener); - initLanguage(position, uploadMediaDetail); - - if (fragment != null) { - FrameLayout.LayoutParams newLayoutParams = (FrameLayout.LayoutParams) clParent.getLayoutParams(); - newLayoutParams.topMargin = 0; - newLayoutParams.leftMargin = 0; - newLayoutParams.rightMargin = 0; - newLayoutParams.bottomMargin = 0; - clParent.setLayoutParams(newLayoutParams); - } - updateAddButtonVisibility(); - addButton.setOnClickListener(v -> eventListener.addLanguage()); - - //If the description was manually added by the user, it deserves focus, if not, let the user decide - if (uploadMediaDetail.isManuallyAdded()) { - captionItemEditText.requestFocus(); - } else { - captionItemEditText.clearFocus(); - } - } - - - private void initLanguage(int position, UploadMediaDetail description) { - - final List recentLanguages = recentLanguagesDao.getRecentLanguages(); - - LanguagesAdapter languagesAdapter = new LanguagesAdapter( - descriptionLanguages.getContext(), - selectedLanguages - ); - - descriptionLanguages.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View view) { - Dialog dialog = new Dialog(view.getContext()); - dialog.setContentView(R.layout.dialog_select_language); - dialog.setCancelable(false); - dialog.getWindow().setLayout( - (int) (view.getContext().getResources().getDisplayMetrics().widthPixels - * 0.90), - (int) (view.getContext().getResources().getDisplayMetrics().heightPixels - * 0.90)); - dialog.show(); - - EditText editText = dialog.findViewById(R.id.search_language); - ListView listView = dialog.findViewById(R.id.language_list); - final Button cancelButton = dialog.findViewById(R.id.cancel_button); - languageHistoryListView = dialog.findViewById(R.id.language_history_list); - recentLanguagesTextView = dialog.findViewById(R.id.recent_searches); - separator = dialog.findViewById(R.id.separator); - setUpRecentLanguagesSection(recentLanguages); - - listView.setAdapter(languagesAdapter); - - cancelButton.setOnClickListener(v -> dialog.dismiss()); - - editText.addTextChangedListener(new TextWatcher() { - @Override - public void beforeTextChanged(CharSequence charSequence, int i, int i1, - int i2) { - hideRecentLanguagesSection(); - } - - @Override - public void onTextChanged(CharSequence charSequence, int i, int i1, - int i2) { - languagesAdapter.getFilter().filter(charSequence); - } - - @Override - public void afterTextChanged(Editable editable) { - - } - }); - - languageHistoryListView.setOnItemClickListener( - (adapterView, view1, position, id) -> { - onRecentLanguageClicked(dialog, adapterView, position, description); - }); - - listView.setOnItemClickListener(new OnItemClickListener() { - @Override - public void onItemClick(AdapterView adapterView, View view, int i, - long l) { - description.setSelectedLanguageIndex(i); - String languageCode = ((LanguagesAdapter) adapterView.getAdapter()) - .getLanguageCode(i); - description.setLanguageCode(languageCode); - final String languageName - = ((LanguagesAdapter) adapterView.getAdapter()).getLanguageName(i); - final boolean isExists - = recentLanguagesDao.findRecentLanguage(languageCode); - if (isExists) { - recentLanguagesDao.deleteRecentLanguage(languageCode); - } - recentLanguagesDao - .addRecentLanguage(new Language(languageName, languageCode)); - - selectedLanguages.clear(); - selectedLanguages.put(position, languageCode); - ((LanguagesAdapter) adapterView - .getAdapter()).setSelectedLangCode(languageCode); - Timber.d("Description language code is: " + languageCode); - descriptionLanguages.setText(languageCode); - dialog.dismiss(); - } - }); - - dialog.setOnDismissListener( - dialogInterface -> languagesAdapter.getFilter().filter("")); - - } - }); - - if (description.getSelectedLanguageIndex() == -1) { - if (!TextUtils.isEmpty(savedLanguageValue)) { - // If user has chosen a default language from settings activity - // savedLanguageValue is not null - if (!TextUtils.isEmpty(description.getLanguageCode())) { - descriptionLanguages.setText(description.getLanguageCode()); - selectedLanguages.remove(position); - selectedLanguages.put(position, description.getLanguageCode()); - } else { - description.setLanguageCode(savedLanguageValue); - descriptionLanguages.setText(savedLanguageValue); - selectedLanguages.remove(position); - selectedLanguages.put(position, savedLanguageValue); - } - } else if (!TextUtils.isEmpty(description.getLanguageCode())) { - descriptionLanguages.setText(description.getLanguageCode()); - selectedLanguages.remove(position); - selectedLanguages.put(position, description.getLanguageCode()); - } else { - //Checking whether Language Code attribute is null or not. - if (uploadMediaDetails.get(position).getLanguageCode() != null) { - //If it is not null that means it is fetching details from the previous - // upload (i.e. when user has pressed copy previous caption & description) - //hence providing same language code for the current upload. - descriptionLanguages.setText(uploadMediaDetails.get(position) - .getLanguageCode()); - selectedLanguages.remove(position); - selectedLanguages.put(position, uploadMediaDetails.get(position) - .getLanguageCode()); - } else { - if (position == 0) { - final int defaultLocaleIndex = languagesAdapter - .getIndexOfUserDefaultLocale(descriptionLanguages - .getContext()); - descriptionLanguages - .setText(languagesAdapter.getLanguageCode(defaultLocaleIndex)); - description.setLanguageCode( - languagesAdapter.getLanguageCode(defaultLocaleIndex)); - selectedLanguages.remove(position); - selectedLanguages.put(position, - languagesAdapter.getLanguageCode(defaultLocaleIndex)); - } else { - description.setLanguageCode(languagesAdapter.getLanguageCode(0)); - descriptionLanguages.setText(languagesAdapter.getLanguageCode(0)); - selectedLanguages.remove(position); - selectedLanguages.put(position, languagesAdapter.getLanguageCode(0)); - } - } - } - - } else { - descriptionLanguages.setText(description.getLanguageCode()); - selectedLanguages.remove(position); - selectedLanguages.put(position, description.getLanguageCode()); - } - } - - /** - * Handles click event for recent language section - */ - private void onRecentLanguageClicked(final Dialog dialog, final AdapterView adapterView, - final int position, final UploadMediaDetail description) { - description.setSelectedLanguageIndex(position); - final String languageCode = ((RecentLanguagesAdapter) adapterView.getAdapter()) - .getLanguageCode(position); - description.setLanguageCode(languageCode); - final String languageName = ((RecentLanguagesAdapter) adapterView.getAdapter()) - .getLanguageName(position); - final boolean isExists = recentLanguagesDao.findRecentLanguage(languageCode); - if (isExists) { - recentLanguagesDao.deleteRecentLanguage(languageCode); - } - recentLanguagesDao.addRecentLanguage(new Language(languageName, languageCode)); - - selectedLanguages.clear(); - selectedLanguages.put(position, languageCode); - ((RecentLanguagesAdapter) adapterView - .getAdapter()).setSelectedLangCode(languageCode); - Timber.d("Description language code is: %s", languageCode); - if (descriptionLanguages!=null) { - descriptionLanguages.setText(languageCode); - } - dialog.dismiss(); - } - - /** - * Hides recent languages section - */ - private void hideRecentLanguagesSection() { - languageHistoryListView.setVisibility(View.GONE); - recentLanguagesTextView.setVisibility(View.GONE); - separator.setVisibility(View.GONE); - } - - /** - * Set up recent languages section - * - * @param recentLanguages recently used languages - */ - private void setUpRecentLanguagesSection(final List recentLanguages) { - if (recentLanguages.isEmpty()) { - languageHistoryListView.setVisibility(View.GONE); - recentLanguagesTextView.setVisibility(View.GONE); - separator.setVisibility(View.GONE); - } else { - if (recentLanguages.size() > 5) { - for (int i = recentLanguages.size() - 1; i >= 5; i--) { - recentLanguagesDao.deleteRecentLanguage(recentLanguages.get(i) - .getLanguageCode()); - } - } - languageHistoryListView.setVisibility(View.VISIBLE); - recentLanguagesTextView.setVisibility(View.VISIBLE); - separator.setVisibility(View.VISIBLE); - - if (descriptionLanguages!=null) { - final RecentLanguagesAdapter recentLanguagesAdapter - = new RecentLanguagesAdapter( - descriptionLanguages.getContext(), - recentLanguagesDao.getRecentLanguages(), - selectedLanguages); - languageHistoryListView.setAdapter(recentLanguagesAdapter); - } - } - } - - /** - * Convert Ideographic space to Latin space - * - * @param source the source text - * @return a string with Latin spaces instead of Ideographic spaces - */ - public String convertIdeographicSpaceToLatinSpace(String source) { - Pattern ideographicSpacePattern = Pattern.compile("\\x{3000}"); - return ideographicSpacePattern.matcher(source).replaceAll(" "); - } - - } - - /** - * Hides the visibility of the "Add" button for all items in the RecyclerView except - * the last item in RecyclerView - */ - private void updateAddButtonVisibility() { - int lastItemPosition = getItemCount() - 1; - // Hide Add Button for all items - for (int i = 0; i < getItemCount(); i++) { - if (fragment != null) { - if (fragment.getView() != null) { - ViewHolder holder = (ViewHolder) ((RecyclerView) fragment.getView() - .findViewById(R.id.rv_descriptions)).findViewHolderForAdapterPosition(i); - if (holder != null) { - holder.addButton.setVisibility(View.GONE); - } - } - } else { - if (this.activity != null) { - ViewHolder holder = (ViewHolder) ((RecyclerView) activity.findViewById( - R.id.rv_descriptions_captions)).findViewHolderForAdapterPosition(i); - if (holder != null) { - holder.addButton.setVisibility(View.GONE); - } - } - } - } - - // Show Add Button for the last item - if (fragment != null) { - if (fragment.getView() != null) { - ViewHolder lastItemHolder = (ViewHolder) ((RecyclerView) fragment.getView() - .findViewById(R.id.rv_descriptions)).findViewHolderForAdapterPosition( - lastItemPosition); - if (lastItemHolder != null) { - lastItemHolder.addButton.setVisibility(View.VISIBLE); - } - } - } else { - if (this.activity != null) { - ViewHolder lastItemHolder = (ViewHolder) ((RecyclerView) activity - .findViewById(R.id.rv_descriptions_captions)).findViewHolderForAdapterPosition( - lastItemPosition); - if (lastItemHolder != null) { - lastItemHolder.addButton.setVisibility(View.VISIBLE); - } - } - } - } - - public interface Callback { - - void showAlert(int mediaDetailDescription, int descriptionInfo); - } - - public interface EventListener { - - void onPrimaryCaptionTextChange(boolean isNotEmpty); - - void addLanguage(); - } - - enum SelectedVoiceIcon { - CAPTION, - DESCRIPTION - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadMediaDetailAdapter.kt b/app/src/main/java/fr/free/nrw/commons/upload/UploadMediaDetailAdapter.kt new file mode 100644 index 000000000..05ed5f665 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadMediaDetailAdapter.kt @@ -0,0 +1,563 @@ +package fr.free.nrw.commons.upload + +import android.app.Activity +import android.app.Dialog +import android.content.Intent +import android.speech.RecognizerIntent.ACTION_RECOGNIZE_SPEECH +import android.speech.RecognizerIntent.EXTRA_LANGUAGE +import android.speech.RecognizerIntent.EXTRA_LANGUAGE_MODEL +import android.speech.RecognizerIntent.LANGUAGE_MODEL_FREE_FORM +import android.text.Editable +import android.text.InputFilter +import android.text.TextUtils +import android.text.TextWatcher +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.AdapterView +import android.widget.AdapterView.OnItemClickListener +import android.widget.EditText +import android.widget.FrameLayout +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.ListView +import android.widget.TextView +import androidx.activity.result.ActivityResultLauncher +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.fragment.app.Fragment +import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.textfield.TextInputLayout +import fr.free.nrw.commons.R +import fr.free.nrw.commons.databinding.RowItemDescriptionBinding +import fr.free.nrw.commons.recentlanguages.Language +import fr.free.nrw.commons.recentlanguages.RecentLanguagesAdapter +import fr.free.nrw.commons.recentlanguages.RecentLanguagesDao +import fr.free.nrw.commons.utils.AbstractTextWatcher +import timber.log.Timber +import java.util.Locale +import java.util.regex.Pattern + +class UploadMediaDetailAdapter : RecyclerView.Adapter { + private var uploadMediaDetails: MutableList + private var selectedLanguages: MutableMap + private val savedLanguageValue: String + private var recentLanguagesTextView: TextView? = null + private var separator: View? = null + private var languageHistoryListView: ListView? = null + private var currentPosition = 0 + private var fragment: Fragment? = null + private var activity: Activity? = null + private val voiceInputResultLauncher: ActivityResultLauncher + private var selectedVoiceIcon: SelectedVoiceIcon? = null + var recentLanguagesDao: RecentLanguagesDao + var callback: Callback? = null + var eventListener: EventListener? = null + var items: List + get() = uploadMediaDetails + set(value) { + uploadMediaDetails = value.toMutableList() + selectedLanguages = mutableMapOf() + notifyDataSetChanged() + } + + + constructor( + fragment: Fragment?, + savedLanguageValue: String, + recentLanguagesDao: RecentLanguagesDao, + voiceInputResultLauncher: ActivityResultLauncher + ) { + uploadMediaDetails = ArrayList() + selectedLanguages = mutableMapOf() + this.savedLanguageValue = savedLanguageValue + this.recentLanguagesDao = recentLanguagesDao + this.fragment = fragment + this.voiceInputResultLauncher = voiceInputResultLauncher + } + + constructor( + activity: Activity?, + savedLanguageValue: String, + uploadMediaDetails: MutableList, + recentLanguagesDao: RecentLanguagesDao, + voiceInputResultLauncher: ActivityResultLauncher + ) { + this.uploadMediaDetails = uploadMediaDetails + selectedLanguages = HashMap() + this.savedLanguageValue = savedLanguageValue + this.recentLanguagesDao = recentLanguagesDao + this.activity = activity + this.voiceInputResultLauncher = voiceInputResultLauncher + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val inflater = LayoutInflater.from(parent.context) + return ViewHolder(RowItemDescriptionBinding.inflate(inflater, parent, false)) + } + + /** + * This is a workaround for a known bug by android here + * https://issuetracker.google.com/issues/37095917 makes the edit text on second and subsequent + * fragments inside an adapter receptive to long click for copy/paste options + * + * @param holder the view holder + */ + override fun onViewAttachedToWindow(holder: ViewHolder) { + super.onViewAttachedToWindow(holder) + holder.binding.captionItemEditText.isEnabled = false + holder.binding.captionItemEditText.isEnabled = true + holder.binding.descriptionItemEditText.isEnabled = false + holder.binding.descriptionItemEditText.isEnabled = true + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + holder.bind(position) + } + + override fun getItemCount(): Int { + return uploadMediaDetails.size + } + + fun addDescription(uploadMediaDetail: UploadMediaDetail) { + selectedLanguages[uploadMediaDetails.size] = "en" + uploadMediaDetails.add(uploadMediaDetail) + notifyItemInserted(uploadMediaDetails.size) + } + + private fun startSpeechInput(locale: String) { + try { + voiceInputResultLauncher.launch(Intent(ACTION_RECOGNIZE_SPEECH).apply { + putExtra(EXTRA_LANGUAGE_MODEL, LANGUAGE_MODEL_FREE_FORM) + putExtra(EXTRA_LANGUAGE, locale) + }) + } catch (e: Exception) { + Timber.e(e) + } + } + + /** + * Handles the result of the speech input by processing the spoken text. + * If the spoken text is not empty, it capitalizes the first letter of the spoken text + * and updates the appropriate field (caption or description) of the current + * UploadMediaDetail based on the selected voice icon. + * Finally, it notifies the adapter that the data set has changed. + * + * @param spokenText the text input received from speech recognition. + */ + fun handleSpeechResult(spokenText: String) { + if (spokenText.isNotEmpty()) { + val spokenTextCapitalized = + spokenText.substring(0, 1).uppercase(Locale.getDefault()) + spokenText.substring(1) + if (currentPosition < uploadMediaDetails.size) { + val uploadMediaDetail = uploadMediaDetails[currentPosition] + when (selectedVoiceIcon) { + SelectedVoiceIcon.CAPTION -> uploadMediaDetail.captionText = + spokenTextCapitalized + + SelectedVoiceIcon.DESCRIPTION -> uploadMediaDetail.descriptionText = + spokenTextCapitalized + + null -> {} + } + notifyDataSetChanged() + } + } + } + + /** + * Remove description based on position from the list and notifies the RecyclerView Adapter that + * data in adapter has been removed at that particular position. + */ + fun removeDescription(uploadMediaDetail: UploadMediaDetail, position: Int) { + selectedLanguages.remove(position) + uploadMediaDetails.remove(uploadMediaDetail) + var i = position + 1 + while (selectedLanguages.containsKey(i)) { + selectedLanguages.remove(i) + i++ + } + notifyItemRemoved(position) + notifyItemRangeChanged(position, uploadMediaDetails.size - position) + updateAddButtonVisibility() + } + + inner class ViewHolder(val binding: RowItemDescriptionBinding) : + RecyclerView.ViewHolder(binding.root) { + + var addButton: ImageView? = null + + var clParent: ConstraintLayout? = null + + var betterCaptionLinearLayout: LinearLayout? = null + + var betterDescriptionLinearLayout: LinearLayout? = null + + private var captionListener: AbstractTextWatcher? = null + + var descriptionListener: AbstractTextWatcher? = null + + fun bind(position: Int) { + val uploadMediaDetail = uploadMediaDetails[position] + Timber.d("UploadMediaDetail is %s", uploadMediaDetail) + + addButton = binding.btnAdd + clParent = binding.clParent + betterCaptionLinearLayout = binding.llWriteBetterCaption + betterDescriptionLinearLayout = binding.llWriteBetterDescription + + + binding.descriptionLanguages.isFocusable = false + binding.captionItemEditText.addTextChangedListener(AbstractTextWatcher { value: String -> + if (position == 0) { + eventListener!!.onPrimaryCaptionTextChange(value.length != 0) + } + }) + binding.captionItemEditText.removeTextChangedListener(captionListener) + binding.descriptionItemEditText.removeTextChangedListener(descriptionListener) + binding.captionItemEditText.setText(uploadMediaDetail.captionText) + binding.descriptionItemEditText.setText(uploadMediaDetail.descriptionText) + binding.captionItemEditTextInputLayout.endIconMode = TextInputLayout.END_ICON_CUSTOM + binding.captionItemEditTextInputLayout.setEndIconDrawable(R.drawable.baseline_keyboard_voice) + binding.captionItemEditTextInputLayout.setEndIconOnClickListener { v: View? -> + currentPosition = position + selectedVoiceIcon = SelectedVoiceIcon.CAPTION + startSpeechInput(binding.descriptionLanguages.text.toString()) + } + binding.descriptionItemEditTextInputLayout.endIconMode = TextInputLayout.END_ICON_CUSTOM + binding.descriptionItemEditTextInputLayout.setEndIconDrawable(R.drawable.baseline_keyboard_voice) + binding.descriptionItemEditTextInputLayout.setEndIconOnClickListener { v: View? -> + currentPosition = position + selectedVoiceIcon = SelectedVoiceIcon.DESCRIPTION + startSpeechInput(binding.descriptionLanguages.text.toString()) + } + + if (position == 0) { + binding.btnRemove.visibility = View.GONE + betterCaptionLinearLayout!!.visibility = View.VISIBLE + betterCaptionLinearLayout!!.setOnClickListener { v: View? -> + callback!!.showAlert( + R.string.media_detail_caption, + R.string.caption_info + ) + } + betterDescriptionLinearLayout!!.visibility = View.VISIBLE + betterDescriptionLinearLayout!!.setOnClickListener { v: View? -> + callback!!.showAlert( + R.string.media_detail_description, + R.string.description_info + ) + } + + binding.captionItemEditTextInputLayout.editText?.let { + it.filters = arrayOf(UploadMediaDetailInputFilter()) + } + } else { + binding.btnRemove.visibility = View.VISIBLE + betterCaptionLinearLayout!!.visibility = View.GONE + betterDescriptionLinearLayout!!.visibility = View.GONE + } + + binding.btnRemove.setOnClickListener { v: View? -> + removeDescription( + uploadMediaDetail, + position + ) + } + captionListener = AbstractTextWatcher { captionText: String -> + uploadMediaDetail.captionText = + convertIdeographicSpaceToLatinSpace(captionText.trim()) + } + descriptionListener = AbstractTextWatcher { value: String? -> + uploadMediaDetail.descriptionText = value + } + binding.captionItemEditText.addTextChangedListener(captionListener) + initLanguage(position, uploadMediaDetail) + + binding.descriptionItemEditText.addTextChangedListener(descriptionListener) + initLanguage(position, uploadMediaDetail) + + if (fragment != null) { + val newLayoutParams = clParent!!.layoutParams as FrameLayout.LayoutParams + newLayoutParams.topMargin = 0 + newLayoutParams.leftMargin = 0 + newLayoutParams.rightMargin = 0 + newLayoutParams.bottomMargin = 0 + clParent!!.layoutParams = newLayoutParams + } + updateAddButtonVisibility() + addButton!!.setOnClickListener { v: View? -> eventListener!!.addLanguage() } + + //If the description was manually added by the user, it deserves focus, if not, let the user decide + if (uploadMediaDetail.isManuallyAdded) { + binding.captionItemEditText.requestFocus() + } else { + binding.captionItemEditText.clearFocus() + } + } + + + private fun initLanguage(position: Int, description: UploadMediaDetail) { + val recentLanguages = recentLanguagesDao.getRecentLanguages() + + val languagesAdapter = LanguagesAdapter( + binding.descriptionLanguages.context, + selectedLanguages + ) + + binding.descriptionLanguages.setOnClickListener { view -> + val dialog = Dialog(view.context) + dialog.setContentView(R.layout.dialog_select_language) + dialog.setCancelable(false) + dialog.window!!.setLayout( + (view.context.resources.displayMetrics.widthPixels + * 0.90).toInt(), + (view.context.resources.displayMetrics.heightPixels + * 0.90).toInt() + ) + dialog.show() + + val editText = + dialog.findViewById(R.id.search_language) + val listView = + dialog.findViewById(R.id.language_list) + languageHistoryListView = + dialog.findViewById(R.id.language_history_list) + recentLanguagesTextView = + dialog.findViewById(R.id.recent_searches) + separator = + dialog.findViewById(R.id.separator) + setUpRecentLanguagesSection(recentLanguages) + + listView.adapter = languagesAdapter + + editText.addTextChangedListener(object : TextWatcher { + override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) = + hideRecentLanguagesSection() + + override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) { + languagesAdapter.filter.filter(charSequence) + } + + override fun afterTextChanged(editable: Editable) = Unit + }) + + languageHistoryListView?.setOnItemClickListener { adapterView: AdapterView<*>, view1: View?, position: Int, id: Long -> + onRecentLanguageClicked(dialog, adapterView, position, description) + } + + listView.onItemClickListener = OnItemClickListener { adapterView, _, i, l -> + description.selectedLanguageIndex = i + val languageCode = (adapterView.adapter as LanguagesAdapter).getLanguageCode(i) + description.languageCode = languageCode + val languageName = (adapterView.adapter as LanguagesAdapter).getLanguageName(i) + val isExists = recentLanguagesDao.findRecentLanguage(languageCode) + if (isExists) { + recentLanguagesDao.deleteRecentLanguage(languageCode) + } + recentLanguagesDao.addRecentLanguage(Language(languageName, languageCode)) + + selectedLanguages.clear() + selectedLanguages[position] = languageCode + (adapterView.adapter as LanguagesAdapter).selectedLangCode = languageCode + Timber.d("Description language code is: %s", languageCode) + binding.descriptionLanguages.text = languageCode + dialog.dismiss() + } + + dialog.setOnDismissListener { + languagesAdapter.filter.filter("") + } + } + + if (description.selectedLanguageIndex == -1) { + if (!TextUtils.isEmpty(savedLanguageValue)) { + // If user has chosen a default language from settings activity + // savedLanguageValue is not null + if (!TextUtils.isEmpty(description.languageCode)) { + binding.descriptionLanguages.text = description.languageCode + selectedLanguages.remove(position) + selectedLanguages[position] = description.languageCode!! + } else { + description.languageCode = savedLanguageValue + binding.descriptionLanguages.text = savedLanguageValue + selectedLanguages.remove(position) + selectedLanguages[position] = savedLanguageValue + } + } else if (!TextUtils.isEmpty(description.languageCode)) { + binding.descriptionLanguages.text = description.languageCode + selectedLanguages.remove(position) + selectedLanguages[position] = description.languageCode!! + } else { + //Checking whether Language Code attribute is null or not. + if (uploadMediaDetails[position].languageCode != null) { + //If it is not null that means it is fetching details from the previous + // upload (i.e. when user has pressed copy previous caption & description) + //hence providing same language code for the current upload. + binding.descriptionLanguages.text = uploadMediaDetails[position] + .languageCode + selectedLanguages.remove(position) + selectedLanguages[position] = uploadMediaDetails[position].languageCode!! + } else { + if (position == 0) { + val defaultLocaleIndex = languagesAdapter.getIndexOfUserDefaultLocale( + binding.descriptionLanguages.getContext()) + binding.descriptionLanguages.setText(languagesAdapter.getLanguageCode(defaultLocaleIndex)) + description.languageCode = languagesAdapter.getLanguageCode(defaultLocaleIndex) + selectedLanguages.remove(position) + selectedLanguages[position] = + languagesAdapter.getLanguageCode(defaultLocaleIndex) + } else { + description.languageCode = languagesAdapter.getLanguageCode(0) + binding.descriptionLanguages.text = languagesAdapter.getLanguageCode(0) + selectedLanguages.remove(position) + selectedLanguages[position] = languagesAdapter.getLanguageCode(0) + } + } + } + } else { + binding.descriptionLanguages.text = description.languageCode + selectedLanguages.remove(position) + description.languageCode?.let { + selectedLanguages[position] = it + } + } + } + + /** + * Handles click event for recent language section + */ + private fun onRecentLanguageClicked( + dialog: Dialog, adapterView: AdapterView<*>, + position: Int, description: UploadMediaDetail + ) { + description.selectedLanguageIndex = position + val languageCode = (adapterView.adapter as RecentLanguagesAdapter) + .getLanguageCode(position) + description.languageCode = languageCode + val languageName = (adapterView.adapter as RecentLanguagesAdapter) + .getLanguageName(position) + val isExists = recentLanguagesDao.findRecentLanguage(languageCode) + if (isExists) { + recentLanguagesDao.deleteRecentLanguage(languageCode) + } + recentLanguagesDao.addRecentLanguage(Language(languageName, languageCode)) + + selectedLanguages.clear() + selectedLanguages[position] = languageCode + (adapterView + .adapter as RecentLanguagesAdapter).selectedLangCode = languageCode + Timber.d("Description language code is: %s", languageCode) + binding.descriptionLanguages.text = languageCode + dialog.dismiss() + } + + /** + * Hides recent languages section + */ + private fun hideRecentLanguagesSection() { + languageHistoryListView!!.visibility = View.GONE + recentLanguagesTextView!!.visibility = View.GONE + separator!!.visibility = View.GONE + } + + /** + * Set up recent languages section + * + * @param recentLanguages recently used languages + */ + private fun setUpRecentLanguagesSection(recentLanguages: List) { + if (recentLanguages.isEmpty()) { + languageHistoryListView!!.visibility = View.GONE + recentLanguagesTextView!!.visibility = View.GONE + separator!!.visibility = View.GONE + } else { + if (recentLanguages.size > 5) { + for (i in recentLanguages.size - 1 downTo 5) { + recentLanguagesDao.deleteRecentLanguage( + recentLanguages[i] + .languageCode + ) + } + } + languageHistoryListView!!.visibility = View.VISIBLE + recentLanguagesTextView!!.visibility = View.VISIBLE + separator!!.visibility = View.VISIBLE + + val recentLanguagesAdapter = RecentLanguagesAdapter( + binding.descriptionLanguages.context, + recentLanguagesDao.getRecentLanguages(), + selectedLanguages + ) + languageHistoryListView!!.adapter = recentLanguagesAdapter + } + } + + /** + * Convert Ideographic space to Latin space + * + * @param source the source text + * @return a string with Latin spaces instead of Ideographic spaces + */ + fun convertIdeographicSpaceToLatinSpace(source: String): String { + val ideographicSpacePattern = Pattern.compile("\\x{3000}") + return ideographicSpacePattern.matcher(source).replaceAll(" ") + } + } + + /** + * Hides the visibility of the "Add" button for all items in the RecyclerView except + * the last item in RecyclerView + */ + private fun updateAddButtonVisibility() { + val lastItemPosition = itemCount - 1 + // Hide Add Button for all items + for (i in 0 until itemCount) { + if (fragment != null) { + if (fragment!!.view != null) { + val holder = (fragment!!.requireView().findViewById(R.id.rv_descriptions) as RecyclerView).findViewHolderForAdapterPosition(i) as ViewHolder? + if (holder != null) { + holder.addButton!!.visibility = View.GONE + } + } + } else { + if (activity != null) { + val holder = (activity!!.findViewById(R.id.rv_descriptions_captions) as RecyclerView).findViewHolderForAdapterPosition(i) as ViewHolder? + if (holder != null) { + holder.addButton!!.visibility = View.GONE + } + } + } + } + + // Show Add Button for the last item + if (fragment != null) { + if (fragment!!.view != null) { + val lastItemHolder = (fragment!!.requireView().findViewById(R.id.rv_descriptions) as RecyclerView).findViewHolderForAdapterPosition(lastItemPosition) as ViewHolder? + if (lastItemHolder != null) { + lastItemHolder.addButton!!.visibility = View.VISIBLE + } + } + } else { + if (activity != null) { + val lastItemHolder = (activity!!.findViewById(R.id.rv_descriptions_captions) as RecyclerView).findViewHolderForAdapterPosition(lastItemPosition) as ViewHolder? + if (lastItemHolder != null) { + lastItemHolder.addButton!!.visibility = View.VISIBLE + } + } + } + } + + fun interface Callback { + fun showAlert(mediaDetailDescription: Int, descriptionInfo: Int) + } + + interface EventListener { + fun onPrimaryCaptionTextChange(isNotEmpty: Boolean) + fun addLanguage() + } + + internal enum class SelectedVoiceIcon { + CAPTION, + DESCRIPTION + } +} diff --git a/app/src/test/kotlin/fr/free/nrw/commons/settings/SettingsFragmentUnitTests.kt b/app/src/test/kotlin/fr/free/nrw/commons/settings/SettingsFragmentUnitTests.kt index 5a6d27e1b..acd56c5f4 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/settings/SettingsFragmentUnitTests.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/settings/SettingsFragmentUnitTests.kt @@ -223,7 +223,7 @@ class SettingsFragmentUnitTests { RecentLanguagesAdapter( context, listOf(Language("English", "en")), - hashMapOf(), + mutableMapOf(), ), ) val method: Method = diff --git a/app/src/test/kotlin/fr/free/nrw/commons/upload/LanguagesAdapterTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/upload/LanguagesAdapterTest.kt index f272a8288..f1b0fe7b8 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/upload/LanguagesAdapterTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/upload/LanguagesAdapterTest.kt @@ -30,7 +30,7 @@ class LanguagesAdapterTest { private lateinit var context: Context @Mock - private lateinit var selectedLanguages: HashMap + private lateinit var selectedLanguages: MutableMap @Mock private lateinit var parent: ViewGroup @@ -41,7 +41,7 @@ class LanguagesAdapterTest { private lateinit var languagesAdapter: LanguagesAdapter private lateinit var convertView: View - private var selectLanguages: HashMap = HashMap() + private var selectLanguages: MutableMap = mutableMapOf() @Before @Throws(Exception::class) @@ -94,8 +94,8 @@ class LanguagesAdapterTest { @Test fun testSelectLanguageNotEmpty() { - selectLanguages[Integer(0)] = "es" - selectLanguages[Integer(1)] = "de" + selectLanguages[0] = "es" + selectLanguages[1] = "de" languagesAdapter = LanguagesAdapter(context, selectLanguages) Assertions.assertEquals(false, languagesAdapter.isEnabled(languagesAdapter.getIndexOfLanguageCode("es"))) diff --git a/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadMediaDetailAdapterUnitTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadMediaDetailAdapterUnitTest.kt index c6bed9bc5..7cc59b78d 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadMediaDetailAdapterUnitTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadMediaDetailAdapterUnitTest.kt @@ -246,7 +246,7 @@ class UploadMediaDetailAdapterUnitTest { RecentLanguagesAdapter( context, listOf(Language("English", "en")), - hashMapOf(), + mutableMapOf(), ), ) val method: Method =