Convert UploadMediaDetailAdapter to kotlin

This commit is contained in:
Paul Hawke 2024-12-23 21:07:57 -06:00
parent a6c4731f74
commit 87c8224793
8 changed files with 577 additions and 652 deletions

View file

@ -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<UploadMediaDetail>? = null
private var descriptionAndCaptions: MutableList<UploadMediaDetail>? = 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<UploadMediaDetail>?) {
private fun initRecyclerView(descriptionAndCaptions: MutableList<UploadMediaDetail>?) {
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

View file

@ -17,7 +17,7 @@ import java.util.HashMap
class RecentLanguagesAdapter constructor(
context: Context,
var recentLanguages: List<Language>,
private val selectedLanguages: HashMap<*, String>,
private val selectedLanguages: MutableMap<Int, String>,
) : ArrayAdapter<String?>(context, R.layout.row_item_languages_spinner) {
/**
* Selected language code in UploadMediaDetailAdapter

View file

@ -23,7 +23,7 @@ import java.util.Locale
*/
class LanguagesAdapter constructor(
context: Context,
private val selectedLanguages: HashMap<*, String>,
private val selectedLanguages: MutableMap<Int, String>,
) : ArrayAdapter<String?>(context, R.layout.row_item_languages_spinner) {
companion object {
/**

View file

@ -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<UploadMediaDetailAdapter.ViewHolder> {
RecentLanguagesDao recentLanguagesDao;
private List<UploadMediaDetail> uploadMediaDetails;
private Callback callback;
private EventListener eventListener;
private HashMap<Integer, String> 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<Intent> voiceInputResultLauncher;
private SelectedVoiceIcon selectedVoiceIcon;
private RowItemDescriptionBinding binding;
public UploadMediaDetailAdapter(Fragment fragment, String savedLanguageValue,
RecentLanguagesDao recentLanguagesDao, ActivityResultLauncher<Intent> 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<UploadMediaDetail> uploadMediaDetails, RecentLanguagesDao recentLanguagesDao, ActivityResultLauncher<Intent> 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<UploadMediaDetail> uploadMediaDetails) {
this.uploadMediaDetails = uploadMediaDetails;
selectedLanguages = new HashMap<>();
notifyDataSetChanged();
}
public List<UploadMediaDetail> 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<Language> 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<Language> 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
}
}

View file

@ -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<UploadMediaDetailAdapter.ViewHolder> {
private var uploadMediaDetails: MutableList<UploadMediaDetail>
private var selectedLanguages: MutableMap<Int, String>
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<Intent>
private var selectedVoiceIcon: SelectedVoiceIcon? = null
var recentLanguagesDao: RecentLanguagesDao
var callback: Callback? = null
var eventListener: EventListener? = null
var items: List<UploadMediaDetail>
get() = uploadMediaDetails
set(value) {
uploadMediaDetails = value.toMutableList()
selectedLanguages = mutableMapOf()
notifyDataSetChanged()
}
constructor(
fragment: Fragment?,
savedLanguageValue: String,
recentLanguagesDao: RecentLanguagesDao,
voiceInputResultLauncher: ActivityResultLauncher<Intent>
) {
uploadMediaDetails = ArrayList()
selectedLanguages = mutableMapOf()
this.savedLanguageValue = savedLanguageValue
this.recentLanguagesDao = recentLanguagesDao
this.fragment = fragment
this.voiceInputResultLauncher = voiceInputResultLauncher
}
constructor(
activity: Activity?,
savedLanguageValue: String,
uploadMediaDetails: MutableList<UploadMediaDetail>,
recentLanguagesDao: RecentLanguagesDao,
voiceInputResultLauncher: ActivityResultLauncher<Intent>
) {
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<InputFilter>(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<EditText>(R.id.search_language)
val listView =
dialog.findViewById<ListView>(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<Language>) {
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<View>(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<View>(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<View>(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<View>(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
}
}

View file

@ -223,7 +223,7 @@ class SettingsFragmentUnitTests {
RecentLanguagesAdapter(
context,
listOf(Language("English", "en")),
hashMapOf<String, String>(),
mutableMapOf(),
),
)
val method: Method =

View file

@ -30,7 +30,7 @@ class LanguagesAdapterTest {
private lateinit var context: Context
@Mock
private lateinit var selectedLanguages: HashMap<Integer, String>
private lateinit var selectedLanguages: MutableMap<Int, String>
@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<Integer, String> = HashMap()
private var selectLanguages: MutableMap<Int, String> = 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")))

View file

@ -246,7 +246,7 @@ class UploadMediaDetailAdapterUnitTest {
RecentLanguagesAdapter(
context,
listOf(Language("English", "en")),
hashMapOf<String, String>(),
mutableMapOf(),
),
)
val method: Method =