Added voice input for caption and description (#5415)

* Fixed Grey empty screen at Upload wizard caption step after denying files permission

* Empty commit

* Fixed loop issue

* Created docs for earlier commits

* Fixed javadoc

* Fixed spaces

* Added added basic features to OSM Maps

* Added search location feature

* Added filter to Open Street Maps

* Fixed chipGroup in Open Street Maps

* Removed mapBox code

* Removed mapBox's code

* Reformat code

* Reformatted code

* Removed rotation feature to map

* Removed rotation files and Fixed Marker click problem

* Ignored failing tests

* Added voice input feature

* Fixed test cases

* Changed caption and description text
This commit is contained in:
Kanahia 2024-01-15 10:52:25 +05:30 committed by GitHub
parent e5c789e874
commit b7090d90c4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 296 additions and 108 deletions

View file

@ -1,9 +1,11 @@
package fr.free.nrw.commons.description package fr.free.nrw.commons.description
import android.app.Activity.RESULT_OK
import android.app.ProgressDialog import android.app.ProgressDialog
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.os.Parcelable import android.os.Parcelable
import android.speech.RecognizerIntent
import android.view.View import android.view.View
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
@ -18,6 +20,7 @@ import fr.free.nrw.commons.theme.BaseActivity
import fr.free.nrw.commons.upload.UploadMediaDetail import fr.free.nrw.commons.upload.UploadMediaDetail
import fr.free.nrw.commons.upload.UploadMediaDetailAdapter import fr.free.nrw.commons.upload.UploadMediaDetailAdapter
import fr.free.nrw.commons.utils.DialogUtil.showAlertDialog import fr.free.nrw.commons.utils.DialogUtil.showAlertDialog
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
/** /**
@ -55,6 +58,8 @@ class DescriptionEditActivity : BaseActivity(), UploadMediaDetailAdapter.EventLi
private lateinit var binding: ActivityDescriptionEditBinding private lateinit var binding: ActivityDescriptionEditBinding
private val REQUEST_CODE_FOR_VOICE_INPUT = 1213
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -78,7 +83,7 @@ class DescriptionEditActivity : BaseActivity(), UploadMediaDetailAdapter.EventLi
* @param descriptionAndCaptions list of description and caption * @param descriptionAndCaptions list of description and caption
*/ */
private fun initRecyclerView(descriptionAndCaptions: ArrayList<UploadMediaDetail>?) { private fun initRecyclerView(descriptionAndCaptions: ArrayList<UploadMediaDetail>?) {
uploadMediaDetailAdapter = UploadMediaDetailAdapter( uploadMediaDetailAdapter = UploadMediaDetailAdapter(this,
savedLanguageValue, descriptionAndCaptions, recentLanguagesDao) savedLanguageValue, descriptionAndCaptions, recentLanguagesDao)
uploadMediaDetailAdapter.setCallback { titleStringID: Int, messageStringId: Int -> uploadMediaDetailAdapter.setCallback { titleStringID: Int, messageStringId: Int ->
showInfoAlert( showInfoAlert(
@ -175,4 +180,15 @@ class DescriptionEditActivity : BaseActivity(), UploadMediaDetailAdapter.EventLi
progressDialog!!.setCanceledOnTouchOutside(false) progressDialog!!.setCanceledOnTouchOutside(false)
progressDialog!!.show() progressDialog!!.show()
} }
override
fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_CODE_FOR_VOICE_INPUT) {
if (resultCode == RESULT_OK && data != null) {
val result = data.getStringArrayListExtra( RecognizerIntent.EXTRA_RESULTS )
uploadMediaDetailAdapter.handleSpeechResult(result!![0]) }
else { Timber.e("Error %s", resultCode) }
}
}
} }

View file

@ -1,6 +1,9 @@
package fr.free.nrw.commons.upload; package fr.free.nrw.commons.upload;
import android.app.Activity;
import android.app.Dialog; import android.app.Dialog;
import android.content.Intent;
import android.speech.RecognizerIntent;
import android.text.Editable; import android.text.Editable;
import android.text.InputFilter; import android.text.InputFilter;
import android.text.TextUtils; import android.text.TextUtils;
@ -13,10 +16,12 @@ import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener; import android.widget.AdapterView.OnItemClickListener;
import android.widget.EditText; import android.widget.EditText;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ListView; import android.widget.ListView;
import android.widget.TextView; import android.widget.TextView;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import butterknife.BindView; import butterknife.BindView;
import butterknife.ButterKnife; import butterknife.ButterKnife;
@ -30,11 +35,13 @@ import fr.free.nrw.commons.utils.AbstractTextWatcher;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Locale;
import java.util.Objects; import java.util.Objects;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import timber.log.Timber; import timber.log.Timber;
public class UploadMediaDetailAdapter extends RecyclerView.Adapter<UploadMediaDetailAdapter.ViewHolder> { public class UploadMediaDetailAdapter extends
RecyclerView.Adapter<UploadMediaDetailAdapter.ViewHolder> {
RecentLanguagesDao recentLanguagesDao; RecentLanguagesDao recentLanguagesDao;
@ -47,20 +54,28 @@ public class UploadMediaDetailAdapter extends RecyclerView.Adapter<UploadMediaDe
private TextView recentLanguagesTextView; private TextView recentLanguagesTextView;
private View separator; private View separator;
private ListView languageHistoryListView; private ListView languageHistoryListView;
private int currentPosition;
private Fragment fragment;
private Activity activity;
private SelectedVoiceIcon selectedVoiceIcon;
private static final int REQUEST_CODE_FOR_VOICE_INPUT = 1213;
public UploadMediaDetailAdapter(String savedLanguageValue, RecentLanguagesDao recentLanguagesDao) { public UploadMediaDetailAdapter(Fragment fragment, String savedLanguageValue,
RecentLanguagesDao recentLanguagesDao) {
uploadMediaDetails = new ArrayList<>(); uploadMediaDetails = new ArrayList<>();
selectedLanguages = new HashMap<>(); selectedLanguages = new HashMap<>();
this.savedLanguageValue = savedLanguageValue; this.savedLanguageValue = savedLanguageValue;
this.recentLanguagesDao = recentLanguagesDao; this.recentLanguagesDao = recentLanguagesDao;
this.fragment = fragment;
} }
public UploadMediaDetailAdapter(final String savedLanguageValue, public UploadMediaDetailAdapter(Activity activity, final String savedLanguageValue,
List<UploadMediaDetail> uploadMediaDetails, RecentLanguagesDao recentLanguagesDao) { List<UploadMediaDetail> uploadMediaDetails, RecentLanguagesDao recentLanguagesDao) {
this.uploadMediaDetails = uploadMediaDetails; this.uploadMediaDetails = uploadMediaDetails;
selectedLanguages = new HashMap<>(); selectedLanguages = new HashMap<>();
this.savedLanguageValue = savedLanguageValue; this.savedLanguageValue = savedLanguageValue;
this.recentLanguagesDao = recentLanguagesDao; this.recentLanguagesDao = recentLanguagesDao;
this.activity = activity;
} }
public void setCallback(Callback callback) { public void setCallback(Callback callback) {
@ -77,7 +92,7 @@ public class UploadMediaDetailAdapter extends RecyclerView.Adapter<UploadMediaDe
notifyDataSetChanged(); notifyDataSetChanged();
} }
public List<UploadMediaDetail> getItems(){ public List<UploadMediaDetail> getItems() {
return uploadMediaDetails; return uploadMediaDetails;
} }
@ -85,13 +100,14 @@ public class UploadMediaDetailAdapter extends RecyclerView.Adapter<UploadMediaDe
@Override @Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return new ViewHolder(LayoutInflater.from(parent.getContext()) return new ViewHolder(LayoutInflater.from(parent.getContext())
.inflate(R.layout.row_item_description, parent, false)); .inflate(R.layout.row_item_description, parent, false));
} }
/** /**
* This is a workaround for a known bug by android here https://issuetracker.google.com/issues/37095917 * This is a workaround for a known bug by android here
* makes the edit text on second and subsequent fragments inside an adapter receptive to long click * https://issuetracker.google.com/issues/37095917 makes the edit text on second and subsequent
* for copy/paste options * fragments inside an adapter receptive to long click for copy/paste options
*
* @param holder the view holder * @param holder the view holder
*/ */
@Override @Override
@ -119,9 +135,43 @@ public class UploadMediaDetailAdapter extends RecyclerView.Adapter<UploadMediaDe
notifyItemInserted(uploadMediaDetails.size()); 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 {
if (activity == null){
fragment.startActivityForResult(intent, REQUEST_CODE_FOR_VOICE_INPUT);
}else {
activity.startActivityForResult(intent, REQUEST_CODE_FOR_VOICE_INPUT);
}
} catch (Exception e) {
Timber.e(e.getMessage());
}
}
public void handleSpeechResult(String spokenText) {
if (currentPosition < uploadMediaDetails.size()) {
UploadMediaDetail uploadMediaDetail = uploadMediaDetails.get(currentPosition);
if (selectedVoiceIcon == SelectedVoiceIcon.CAPTION){
uploadMediaDetail.setCaptionText(spokenText);
}else {
uploadMediaDetail.setDescriptionText(spokenText);
}
notifyItemChanged(currentPosition);
}
}
/** /**
* Remove description based on position from the list and notifies the RecyclerView Adapter that * Remove description based on position from the list and notifies the RecyclerView Adapter that
* data in adapter has been removed at that particular position. * data in adapter has been removed at that particular position.
*
* @param uploadMediaDetail * @param uploadMediaDetail
* @param position * @param position
*/ */
@ -160,6 +210,12 @@ public class UploadMediaDetailAdapter extends RecyclerView.Adapter<UploadMediaDe
@BindView(R.id.btn_remove) @BindView(R.id.btn_remove)
ImageView removeButton; ImageView removeButton;
@BindView(R.id.ll_write_better_caption)
LinearLayout betterCaptionLinearLayout;
@BindView(R.id.ll_write_better_description)
LinearLayout betterDescriptionLinearLayout;
AbstractTextWatcher captionListener; AbstractTextWatcher captionListener;
AbstractTextWatcher descriptionListener; AbstractTextWatcher descriptionListener;
@ -185,34 +241,48 @@ public class UploadMediaDetailAdapter extends RecyclerView.Adapter<UploadMediaDe
descItemEditText.removeTextChangedListener(descriptionListener); descItemEditText.removeTextChangedListener(descriptionListener);
captionItemEditText.setText(uploadMediaDetail.getCaptionText()); captionItemEditText.setText(uploadMediaDetail.getCaptionText());
descItemEditText.setText(uploadMediaDetail.getDescriptionText()); 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) { if (position == 0) {
removeButton.setVisibility(View.GONE); removeButton.setVisibility(View.GONE);
captionInputLayout.setEndIconMode(TextInputLayout.END_ICON_CUSTOM); betterCaptionLinearLayout.setVisibility(View.VISIBLE);
captionInputLayout.setEndIconDrawable(R.drawable.maplibre_info_icon_default); betterCaptionLinearLayout.setOnClickListener(
captionInputLayout.setEndIconOnClickListener(v -> v -> callback.showAlert(R.string.media_detail_caption, R.string.caption_info));
callback.showAlert(R.string.media_detail_caption, R.string.caption_info)); betterDescriptionLinearLayout.setVisibility(View.VISIBLE);
Objects.requireNonNull(captionInputLayout.getEditText()).setFilters(new InputFilter[] { betterDescriptionLinearLayout.setOnClickListener(
new UploadMediaDetailInputFilter() v -> callback.showAlert(R.string.media_detail_description,
}); R.string.description_info));
Objects.requireNonNull(captionInputLayout.getEditText())
descInputLayout.setEndIconMode(TextInputLayout.END_ICON_CUSTOM); .setFilters(new InputFilter[]{
descInputLayout.setEndIconDrawable(R.drawable.maplibre_info_icon_default); new UploadMediaDetailInputFilter()
descInputLayout.setEndIconOnClickListener(v -> });
callback.showAlert(R.string.media_detail_description, R.string.description_info));
} else { } else {
removeButton.setVisibility(View.VISIBLE); removeButton.setVisibility(View.VISIBLE);
captionInputLayout.setEndIconDrawable(null); betterCaptionLinearLayout.setVisibility(View.GONE);
descInputLayout.setEndIconDrawable(null); betterDescriptionLinearLayout.setVisibility(View.GONE);
} }
removeButton.setOnClickListener(v -> removeDescription(uploadMediaDetail, position)); removeButton.setOnClickListener(v -> removeDescription(uploadMediaDetail, position));
captionListener = new AbstractTextWatcher( captionListener = new AbstractTextWatcher(
captionText -> uploadMediaDetails.get(position).setCaptionText(convertIdeographicSpaceToLatinSpace( captionText -> uploadMediaDetails.get(position)
removeLeadingAndTrailingWhitespace(captionText)))); .setCaptionText(convertIdeographicSpaceToLatinSpace(
removeLeadingAndTrailingWhitespace(captionText))));
descriptionListener = new AbstractTextWatcher( descriptionListener = new AbstractTextWatcher(
descriptionText -> uploadMediaDetails.get(position).setDescriptionText(descriptionText)); descriptionText -> uploadMediaDetails.get(position)
.setDescriptionText(descriptionText));
captionItemEditText.addTextChangedListener(captionListener); captionItemEditText.addTextChangedListener(captionListener);
initLanguage(position, uploadMediaDetail); initLanguage(position, uploadMediaDetail);
@ -228,23 +298,26 @@ public class UploadMediaDetailAdapter extends RecyclerView.Adapter<UploadMediaDe
} }
private void initLanguage(int position, UploadMediaDetail description) { private void initLanguage(int position, UploadMediaDetail description) {
final List<Language> recentLanguages = recentLanguagesDao.getRecentLanguages(); final List<Language> recentLanguages = recentLanguagesDao.getRecentLanguages();
LanguagesAdapter languagesAdapter = new LanguagesAdapter( LanguagesAdapter languagesAdapter = new LanguagesAdapter(
descriptionLanguages.getContext(), descriptionLanguages.getContext(),
selectedLanguages selectedLanguages
); );
descriptionLanguages.setOnClickListener(new OnClickListener() { descriptionLanguages.setOnClickListener(new OnClickListener() {
@Override @Override
public void onClick(View view) { public void onClick(View view) {
Dialog dialog = new Dialog(view.getContext()); Dialog dialog = new Dialog(view.getContext());
dialog.setContentView(R.layout.dialog_select_language); dialog.setContentView(R.layout.dialog_select_language);
dialog.setCanceledOnTouchOutside(true); dialog.setCanceledOnTouchOutside(true);
dialog.getWindow().setLayout((int)(view.getContext().getResources().getDisplayMetrics().widthPixels*0.90), dialog.getWindow().setLayout(
(int)(view.getContext().getResources().getDisplayMetrics().heightPixels*0.90)); (int) (view.getContext().getResources().getDisplayMetrics().widthPixels
* 0.90),
(int) (view.getContext().getResources().getDisplayMetrics().heightPixels
* 0.90));
dialog.show(); dialog.show();
EditText editText = dialog.findViewById(R.id.search_language); EditText editText = dialog.findViewById(R.id.search_language);
@ -275,9 +348,10 @@ public class UploadMediaDetailAdapter extends RecyclerView.Adapter<UploadMediaDe
} }
}); });
languageHistoryListView.setOnItemClickListener((adapterView, view1, position, id) -> { languageHistoryListView.setOnItemClickListener(
onRecentLanguageClicked(dialog, adapterView, position, description); (adapterView, view1, position, id) -> {
}); onRecentLanguageClicked(dialog, adapterView, position, description);
});
listView.setOnItemClickListener(new OnItemClickListener() { listView.setOnItemClickListener(new OnItemClickListener() {
@Override @Override
@ -317,7 +391,7 @@ public class UploadMediaDetailAdapter extends RecyclerView.Adapter<UploadMediaDe
if (!TextUtils.isEmpty(savedLanguageValue)) { if (!TextUtils.isEmpty(savedLanguageValue)) {
// If user has chosen a default language from settings activity // If user has chosen a default language from settings activity
// savedLanguageValue is not null // savedLanguageValue is not null
if(!TextUtils.isEmpty(description.getLanguageCode())) { if (!TextUtils.isEmpty(description.getLanguageCode())) {
descriptionLanguages.setText(description.getLanguageCode()); descriptionLanguages.setText(description.getLanguageCode());
selectedLanguages.remove(position); selectedLanguages.remove(position);
selectedLanguages.put(position, description.getLanguageCode()); selectedLanguages.put(position, description.getLanguageCode());
@ -349,9 +423,11 @@ public class UploadMediaDetailAdapter extends RecyclerView.Adapter<UploadMediaDe
.getContext()); .getContext());
descriptionLanguages descriptionLanguages
.setText(languagesAdapter.getLanguageCode(defaultLocaleIndex)); .setText(languagesAdapter.getLanguageCode(defaultLocaleIndex));
description.setLanguageCode(languagesAdapter.getLanguageCode(defaultLocaleIndex)); description.setLanguageCode(
languagesAdapter.getLanguageCode(defaultLocaleIndex));
selectedLanguages.remove(position); selectedLanguages.remove(position);
selectedLanguages.put(position, languagesAdapter.getLanguageCode(defaultLocaleIndex)); selectedLanguages.put(position,
languagesAdapter.getLanguageCode(defaultLocaleIndex));
} else { } else {
description.setLanguageCode(languagesAdapter.getLanguageCode(0)); description.setLanguageCode(languagesAdapter.getLanguageCode(0));
descriptionLanguages.setText(languagesAdapter.getLanguageCode(0)); descriptionLanguages.setText(languagesAdapter.getLanguageCode(0));
@ -415,7 +491,7 @@ public class UploadMediaDetailAdapter extends RecyclerView.Adapter<UploadMediaDe
separator.setVisibility(View.GONE); separator.setVisibility(View.GONE);
} else { } else {
if (recentLanguages.size() > 5) { if (recentLanguages.size() > 5) {
for (int i = recentLanguages.size()-1; i >=5; i--) { for (int i = recentLanguages.size() - 1; i >= 5; i--) {
recentLanguagesDao.deleteRecentLanguage(recentLanguages.get(i) recentLanguagesDao.deleteRecentLanguage(recentLanguages.get(i)
.getLanguageCode()); .getLanguageCode());
} }
@ -425,15 +501,16 @@ public class UploadMediaDetailAdapter extends RecyclerView.Adapter<UploadMediaDe
separator.setVisibility(View.VISIBLE); separator.setVisibility(View.VISIBLE);
final RecentLanguagesAdapter recentLanguagesAdapter final RecentLanguagesAdapter recentLanguagesAdapter
= new RecentLanguagesAdapter( = new RecentLanguagesAdapter(
descriptionLanguages.getContext(), descriptionLanguages.getContext(),
recentLanguagesDao.getRecentLanguages(), recentLanguagesDao.getRecentLanguages(),
selectedLanguages); selectedLanguages);
languageHistoryListView.setAdapter(recentLanguagesAdapter); languageHistoryListView.setAdapter(recentLanguagesAdapter);
} }
} }
/** /**
* Removes any leading and trailing whitespace from the source text. * Removes any leading and trailing whitespace from the source text.
*
* @param source input string * @param source input string
* @return a string without leading and trailing whitespace * @return a string without leading and trailing whitespace
*/ */
@ -466,6 +543,7 @@ public class UploadMediaDetailAdapter extends RecyclerView.Adapter<UploadMediaDe
/** /**
* Convert Ideographic space to Latin space * Convert Ideographic space to Latin space
*
* @param source the source text * @param source the source text
* @return a string with Latin spaces instead of Ideographic spaces * @return a string with Latin spaces instead of Ideographic spaces
*/ */
@ -483,5 +561,8 @@ public class UploadMediaDetailAdapter extends RecyclerView.Adapter<UploadMediaDe
public interface EventListener { public interface EventListener {
void onPrimaryCaptionTextChange(boolean isNotEmpty); void onPrimaryCaptionTextChange(boolean isNotEmpty);
} }
enum SelectedVoiceIcon {
CAPTION,
DESCRIPTION
}
} }

View file

@ -6,15 +6,13 @@ import static fr.free.nrw.commons.utils.ImageUtils.FILE_NAME_EXISTS;
import static fr.free.nrw.commons.utils.ImageUtils.getErrorMessageForResult; import static fr.free.nrw.commons.utils.ImageUtils.getErrorMessageForResult;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.speech.RecognizerIntent;
import android.text.TextUtils; import android.text.TextUtils;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.CheckBox; import android.widget.CheckBox;
import android.widget.ImageView; import android.widget.ImageView;
@ -40,7 +38,6 @@ import fr.free.nrw.commons.contributions.MainActivity;
import fr.free.nrw.commons.filepicker.UploadableFile; import fr.free.nrw.commons.filepicker.UploadableFile;
import fr.free.nrw.commons.kvstore.JsonKvStore; import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.location.LatLng; import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.location.LocationServiceManager;
import fr.free.nrw.commons.nearby.Place; import fr.free.nrw.commons.nearby.Place;
import fr.free.nrw.commons.recentlanguages.RecentLanguagesDao; import fr.free.nrw.commons.recentlanguages.RecentLanguagesDao;
import fr.free.nrw.commons.settings.Prefs; import fr.free.nrw.commons.settings.Prefs;
@ -54,8 +51,8 @@ import fr.free.nrw.commons.upload.UploadMediaDetailAdapter;
import fr.free.nrw.commons.utils.DialogUtil; import fr.free.nrw.commons.utils.DialogUtil;
import fr.free.nrw.commons.utils.ImageUtils; import fr.free.nrw.commons.utils.ImageUtils;
import fr.free.nrw.commons.utils.ViewUtil; import fr.free.nrw.commons.utils.ViewUtil;
import fr.free.nrw.commons.R.drawable.*;
import java.io.File; import java.io.File;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Objects; import java.util.Objects;
@ -69,6 +66,7 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
private static final int REQUEST_CODE = 1211; private static final int REQUEST_CODE = 1211;
private static final int REQUEST_CODE_FOR_EDIT_ACTIVITY = 1212; private static final int REQUEST_CODE_FOR_EDIT_ACTIVITY = 1212;
private static final int REQUEST_CODE_FOR_VOICE_INPUT = 1213;
/** /**
* A key for applicationKvStore. By this key we can retrieve the location of last UploadItem ex. * A key for applicationKvStore. By this key we can retrieve the location of last UploadItem ex.
@ -238,7 +236,7 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
* init the description recycler veiw and caption recyclerview * init the description recycler veiw and caption recyclerview
*/ */
private void initRecyclerView() { private void initRecyclerView() {
uploadMediaDetailAdapter = new UploadMediaDetailAdapter( uploadMediaDetailAdapter = new UploadMediaDetailAdapter(this,
defaultKvStore.getString(Prefs.DESCRIPTION_LANGUAGE, ""), recentLanguagesDao); defaultKvStore.getString(Prefs.DESCRIPTION_LANGUAGE, ""), recentLanguagesDao);
uploadMediaDetailAdapter.setCallback(this::showInfoAlert); uploadMediaDetailAdapter.setCallback(this::showInfoAlert);
uploadMediaDetailAdapter.setEventListener(this); uploadMediaDetailAdapter.setEventListener(this);
@ -558,7 +556,6 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
public void onActivityResult(final int requestCode, final int resultCode, public void onActivityResult(final int requestCode, final int resultCode,
@Nullable final Intent data) { @Nullable final Intent data) {
super.onActivityResult(requestCode, resultCode, data); super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_CODE && resultCode == RESULT_OK) { if (requestCode == REQUEST_CODE && resultCode == RESULT_OK) {
assert data != null; assert data != null;
@ -597,6 +594,15 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
Timber.e(e); Timber.e(e);
} }
} }
else if (requestCode == REQUEST_CODE_FOR_VOICE_INPUT) {
if (resultCode == RESULT_OK && data != null) {
ArrayList<String> result = data.getStringArrayListExtra(
RecognizerIntent.EXTRA_RESULTS);
uploadMediaDetailAdapter.handleSpeechResult(result.get(0));
}else {
Timber.e("Error %s", resultCode);
}
}
} }
/** /**

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="21dp"
android:height="21dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="#FF1E8CAB"
android:pathData="M12,15c1.66,0 2.99,-1.34 2.99,-3L15,6c0,-1.66 -1.34,-3 -3,-3S9,4.34 9,6v6c0,1.66 1.34,3 3,3zM17.3,12c0,3 -2.54,5.1 -5.3,5.1S6.7,15 6.7,12L5,12c0,3.42 2.72,6.23 6,6.72L11,22h2v-3.28c3.28,-0.48 6,-3.3 6,-6.72h-1.7z" />
</vector>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true"
android:drawable="@color/pressed_button_light" />
<item android:state_focused="false"
android:drawable="@android:color/transparent" />
</selector>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"
android:viewportWidth="20"
android:tint="?attr/editTextColor"
android:viewportHeight="20">
<path
android:pathData="M17.778,17.778H2.223V2.222H10V0H2.223C0.989,0 0,1 0,2.222V17.778C0,19 0.989,20 2.223,20H17.778C19,20 20,19 20,17.778V10H17.778V17.778ZM12.223,0V2.222H16.212L5.289,13.144L6.856,14.711L17.778,3.789V7.778H20V0H12.223Z"
android:fillColor="#8F000000"/>
</vector>

View file

@ -7,68 +7,118 @@
android:layout_marginVertical="8dp" android:layout_marginVertical="8dp"
app:elevation="6dp"> app:elevation="6dp">
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginHorizontal="20dp"
android:layout_marginVertical="20dp">
<ImageView
android:id="@+id/btn_remove"
android:layout_width="24dp"
android:layout_height="wrap_content"
android:contentDescription="@string/remove"
android:visibility="visible"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_remove" />
<TextView
android:id="@+id/description_languages"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clickable="true"
android:drawableRight="@drawable/ic_baseline_arrow_drop_down_24"
android:padding="@dimen/dimen_2"
android:textSize="18sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/caption_item_edit_text_input_layout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="wrap_content"
android:layout_marginHorizontal="20dp" android:layout_marginTop="4dp"
android:layout_marginVertical="20dp"> app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/description_languages">
<ImageView <fr.free.nrw.commons.ui.PasteSensitiveTextInputEditText
android:id="@+id/btn_remove" android:id="@+id/caption_item_edit_text"
android:layout_width="24dp" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="match_parent"
android:contentDescription="@string/remove" android:hint="@string/share_caption_hint"
android:visibility="visible" android:imeOptions="actionNext|flagNoExtractUi"
app:layout_constraintBottom_toTopOf="@+id/caption_item_edit_text_input_layout" android:inputType="text"
app:layout_constraintEnd_toEndOf="parent" app:allowFormatting="false" />
app:srcCompat="@drawable/ic_remove" /> </com.google.android.material.textfield.TextInputLayout>
<TextView <LinearLayout
android:id="@+id/description_languages" android:id="@+id/ll_write_better_caption"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:clickable="true" android:layout_marginStart="2dp"
android:drawableRight="@drawable/ic_baseline_arrow_drop_down_24" android:background="@drawable/clicked_linearlayout_background"
android:padding="@dimen/dimen_2" android:clickable="true"
android:textSize="18sp" android:gravity="center_vertical"
app:layout_constraintBottom_toTopOf="@+id/caption_item_edit_text_input_layout" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintStart_toStartOf="parent" /> app:layout_constraintTop_toBottomOf="@+id/caption_item_edit_text_input_layout">
<com.google.android.material.textfield.TextInputLayout <TextView
android:id="@+id/caption_item_edit_text_input_layout" android:layout_width="wrap_content"
android:layout_width="match_parent" android:layout_height="wrap_content"
android:layout_height="wrap_content" android:text="@string/learn_how_to_write_a_useful_caption"
app:layout_constraintBottom_toTopOf="@id/description_item_edit_text_input_layout" android:textSize="12sp" />
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
<fr.free.nrw.commons.ui.PasteSensitiveTextInputEditText <ImageView
android:id="@+id/caption_item_edit_text" android:layout_width="10dp"
android:layout_width="match_parent" android:layout_height="10dp"
android:layout_height="match_parent" android:layout_marginStart="6dp"
android:hint="@string/share_caption_hint" app:srcCompat="@drawable/ic_open" />
android:imeOptions="actionNext|flagNoExtractUi"
android:inputType="text"
app:allowFormatting="false" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout </LinearLayout>
android:id="@+id/description_item_edit_text_input_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
<fr.free.nrw.commons.ui.PasteSensitiveTextInputEditText <com.google.android.material.textfield.TextInputLayout
android:id="@+id/description_item_edit_text" android:id="@+id/description_item_edit_text_input_layout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="wrap_content"
android:hint="@string/share_description_hint" android:layout_marginTop="4dp"
android:imeOptions="actionNext|flagNoExtractUi" app:layout_constraintEnd_toEndOf="parent"
android:inputType="textMultiLine" app:layout_constraintStart_toStartOf="parent"
app:allowFormatting="false" /> app:layout_constraintTop_toBottomOf="@+id/ll_write_better_caption">
</com.google.android.material.textfield.TextInputLayout>
</androidx.constraintlayout.widget.ConstraintLayout> <fr.free.nrw.commons.ui.PasteSensitiveTextInputEditText
android:id="@+id/description_item_edit_text"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:hint="@string/share_description_hint"
android:imeOptions="actionNext|flagNoExtractUi"
android:inputType="textMultiLine"
app:allowFormatting="false" />
</com.google.android.material.textfield.TextInputLayout>
<LinearLayout
android:id="@+id/ll_write_better_description"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="2dp"
android:background="@drawable/clicked_linearlayout_background"
android:clickable="true"
android:gravity="center_vertical"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/description_item_edit_text_input_layout">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/learn_how_to_write_a_useful_description"
android:textSize="12sp" />
<ImageView
android:layout_width="10dp"
android:layout_height="10dp"
android:layout_marginStart="6dp"
app:srcCompat="@drawable/ic_open" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView> </androidx.cardview.widget.CardView>

View file

@ -784,6 +784,8 @@ Upload your first media by tapping on the add button.</string>
<string name="storage_permissions_denied">Storage Permissions Denied</string> <string name="storage_permissions_denied">Storage Permissions Denied</string>
<string name="unable_to_share_upload_item">Unable to share this item</string> <string name="unable_to_share_upload_item">Unable to share this item</string>
<string name="permissions_are_required_for_functionality">Permissions are required for functionality</string> <string name="permissions_are_required_for_functionality">Permissions are required for functionality</string>
<string name="learn_how_to_write_a_useful_description">Learn how to write a useful description</string>
<string name="learn_how_to_write_a_useful_caption">Learn how to write a useful caption</string>
<plurals name="custom_picker_images_selected_title_appendix"> <plurals name="custom_picker_images_selected_title_appendix">
<item quantity="one">%d image selected</item> <item quantity="one">%d image selected</item>
<item quantity="other">%d images selected</item> <item quantity="other">%d images selected</item>

View file

@ -17,12 +17,14 @@ import fr.free.nrw.commons.recentlanguages.Language
import fr.free.nrw.commons.recentlanguages.RecentLanguagesAdapter import fr.free.nrw.commons.recentlanguages.RecentLanguagesAdapter
import fr.free.nrw.commons.recentlanguages.RecentLanguagesDao import fr.free.nrw.commons.recentlanguages.RecentLanguagesDao
import fr.free.nrw.commons.settings.SettingsFragment import fr.free.nrw.commons.settings.SettingsFragment
import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment
import org.junit.Assert import org.junit.Assert
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.mockito.Mock import org.mockito.Mock
import org.mockito.Mockito import org.mockito.Mockito
import org.mockito.Mockito.mock
import org.mockito.MockitoAnnotations import org.mockito.MockitoAnnotations
import org.powermock.reflect.Whitebox import org.powermock.reflect.Whitebox
import org.robolectric.Robolectric import org.robolectric.Robolectric
@ -55,6 +57,9 @@ class UploadMediaDetailAdapterUnitTest {
@Mock @Mock
private lateinit var textView: TextView private lateinit var textView: TextView
@Mock
private lateinit var fragment : UploadMediaDetailFragment
@Mock @Mock
private lateinit var view: View private lateinit var view: View
@ -69,7 +74,8 @@ class UploadMediaDetailAdapterUnitTest {
MockitoAnnotations.openMocks(this) MockitoAnnotations.openMocks(this)
uploadMediaDetails = mutableListOf(uploadMediaDetail, uploadMediaDetail) uploadMediaDetails = mutableListOf(uploadMediaDetail, uploadMediaDetail)
activity = Robolectric.buildActivity(UploadActivity::class.java).get() activity = Robolectric.buildActivity(UploadActivity::class.java).get()
adapter = UploadMediaDetailAdapter("", recentLanguagesDao) fragment = mock(UploadMediaDetailFragment::class.java)
adapter = UploadMediaDetailAdapter(fragment,"", recentLanguagesDao)
context = ApplicationProvider.getApplicationContext() context = ApplicationProvider.getApplicationContext()
Whitebox.setInternalState(adapter, "uploadMediaDetails", uploadMediaDetails) Whitebox.setInternalState(adapter, "uploadMediaDetails", uploadMediaDetails)
Whitebox.setInternalState(adapter, "eventListener", eventListener) Whitebox.setInternalState(adapter, "eventListener", eventListener)