Convert UploadMediaDetailFragment to kotlin

This commit is contained in:
Paul Hawke 2024-12-31 17:10:45 -06:00
parent 69f804438e
commit b9c2d79fe7
8 changed files with 933 additions and 1010 deletions

View file

@ -41,8 +41,8 @@ import fr.free.nrw.commons.location.LocationPermissionsHelper
import fr.free.nrw.commons.location.LocationPermissionsHelper.LocationPermissionCallback
import fr.free.nrw.commons.location.LocationServiceManager
import fr.free.nrw.commons.theme.BaseActivity
import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment.LAST_LOCATION
import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment.LAST_ZOOM
import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment.Companion.LAST_LOCATION
import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment.Companion.LAST_ZOOM
import fr.free.nrw.commons.utils.DialogUtil
import fr.free.nrw.commons.utils.MapUtils.ZOOM_LEVEL
import io.reactivex.android.schedulers.AndroidSchedulers

View file

@ -511,11 +511,11 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C
* ensuring that the thumbnail change is reflected in the UI.
*
* @param index The index of the UploadableFile to be updated.
* @param filepath The file path of the new thumbnail image.
* @param uri The file path of the new thumbnail image.
*/
override fun changeThumbnail(index: Int, filepath: String) {
override fun changeThumbnail(index: Int, uri: String) {
uploadableFiles.removeAt(index)
uploadableFiles.add(index, UploadableFile(File(filepath)))
uploadableFiles.add(index, UploadableFile(File(uri)))
binding.rvThumbnails.adapter!!.notifyDataSetChanged()
}
@ -544,9 +544,9 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C
if (isFragmentsSaved) {
val fragment = fragments!![0] as UploadMediaDetailFragment?
fragment!!.setCallback(uploadMediaDetailFragmentCallback)
fragment!!.fragmentCallback = uploadMediaDetailFragmentCallback
} else {
uploadMediaDetailFragment.setCallback(uploadMediaDetailFragmentCallback)
uploadMediaDetailFragment.fragmentCallback = uploadMediaDetailFragmentCallback
fragments!!.add(uploadMediaDetailFragment)
}
}

View file

@ -1,924 +0,0 @@
package fr.free.nrw.commons.upload.mediaDetails;
import static android.app.Activity.RESULT_OK;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Intent;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
import android.os.Parcelable;
import android.speech.RecognizerIntent;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.ImageView;
import android.widget.Toast;
import androidx.activity.result.ActivityResult;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.exifinterface.media.ExifInterface;
import androidx.recyclerview.widget.LinearLayoutManager;
import fr.free.nrw.commons.CameraPosition;
import fr.free.nrw.commons.locationpicker.LocationPicker;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.contributions.MainActivity;
import fr.free.nrw.commons.databinding.FragmentUploadMediaDetailFragmentBinding;
import fr.free.nrw.commons.edit.EditActivity;
import fr.free.nrw.commons.filepicker.UploadableFile;
import fr.free.nrw.commons.kvstore.BasicKvStore;
import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.nearby.Place;
import fr.free.nrw.commons.recentlanguages.RecentLanguagesDao;
import fr.free.nrw.commons.settings.Prefs;
import fr.free.nrw.commons.upload.ImageCoordinates;
import fr.free.nrw.commons.upload.SimilarImageDialogFragment;
import fr.free.nrw.commons.upload.UploadActivity;
import fr.free.nrw.commons.upload.UploadBaseFragment;
import fr.free.nrw.commons.upload.UploadItem;
import fr.free.nrw.commons.upload.UploadMediaDetail;
import fr.free.nrw.commons.upload.UploadMediaDetailAdapter;
import fr.free.nrw.commons.utils.ActivityUtils;
import fr.free.nrw.commons.utils.DialogUtil;
import fr.free.nrw.commons.utils.ImageUtils;
import fr.free.nrw.commons.utils.NetworkUtils;
import fr.free.nrw.commons.utils.ViewUtil;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import javax.inject.Inject;
import javax.inject.Named;
import org.jetbrains.annotations.NotNull;
import timber.log.Timber;
public class UploadMediaDetailFragment extends UploadBaseFragment implements
UploadMediaDetailsContract.View, UploadMediaDetailAdapter.EventListener {
private UploadMediaDetailAdapter uploadMediaDetailAdapter;
private final ActivityResultLauncher<Intent> startForResult = registerForActivityResult(
new StartActivityForResult(), result -> {
onCameraPosition(result);
});
private final ActivityResultLauncher<Intent> startForEditActivityResult = registerForActivityResult(
new StartActivityForResult(), result -> {
onEditActivityResult(result);
}
);
private final ActivityResultLauncher<Intent> voiceInputResultLauncher = registerForActivityResult(
new StartActivityForResult(), result -> {
onVoiceInput(result);
}
);
public static Activity activity ;
private int indexOfFragment;
/**
* A key for applicationKvStore. By this key we can retrieve the location of last UploadItem ex.
* 12.3433,54.78897 from applicationKvStore.
*/
public static final String LAST_LOCATION = "last_location_while_uploading";
public static final String LAST_ZOOM = "last_zoom_level_while_uploading";
public static final String UPLOADABLE_FILE = "uploadable_file";
public static final String UPLOAD_MEDIA_DETAILS = "upload_media_detail_adapter";
/**
* True when user removes location from the current image
*/
private boolean hasUserRemovedLocation;
@Inject
UploadMediaDetailsContract.UserActionListener presenter;
@Inject
@Named("default_preferences")
JsonKvStore defaultKvStore;
@Inject
RecentLanguagesDao recentLanguagesDao;
private UploadableFile uploadableFile;
private Place place;
private boolean isExpanded = true;
/**
* True if location is added via the "missing location" popup dialog (which appears after
* tapping "Next" if the picture has no geographical coordinates).
*/
private boolean isMissingLocationDialog;
/**
* showNearbyFound will be true, if any nearby location found that needs pictures and the nearby
* popup is yet to be shown Used to show and check if the nearby found popup is already shown
*/
private boolean showNearbyFound;
/**
* nearbyPlace holds the detail of nearby place that need pictures, if any found
*/
private Place nearbyPlace;
private UploadItem uploadItem;
/**
* inAppPictureLocation: use location recorded while using the in-app camera if device camera
* does not record it in the EXIF
*/
private LatLng inAppPictureLocation;
/**
* editableUploadItem : Storing the upload item before going to update the coordinates
*/
private UploadItem editableUploadItem;
private BasicKvStore basicKvStore;
private final String keyForShowingAlertDialog = "isNoNetworkAlertDialogShowing";
private UploadMediaDetailFragmentCallback callback;
private FragmentUploadMediaDetailFragmentBinding binding;
public void setCallback(UploadMediaDetailFragmentCallback callback) {
this.callback = callback;
UploadMediaPresenter.Companion.setPresenterCallback(callback);
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if(savedInstanceState!=null && uploadableFile==null) {
uploadableFile = savedInstanceState.getParcelable(UPLOADABLE_FILE);
}
}
public void setImageToBeUploaded(UploadableFile uploadableFile, Place place,
LatLng inAppPictureLocation) {
this.uploadableFile = uploadableFile;
this.place = place;
this.inAppPictureLocation = inAppPictureLocation;
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
binding = FragmentUploadMediaDetailFragmentBinding.inflate(inflater, container, false);
return binding.getRoot();
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
activity = requireActivity();
basicKvStore = new BasicKvStore(activity, "CurrentUploadImageQualities");
if (callback != null) {
indexOfFragment = callback.getIndexInViewFlipper(this);
initializeFragment();
}
if(savedInstanceState!=null){
if(uploadMediaDetailAdapter.getItems().size()==0 && callback != null){
uploadMediaDetailAdapter.setItems(savedInstanceState.getParcelableArrayList(UPLOAD_MEDIA_DETAILS));
presenter.setUploadMediaDetails(uploadMediaDetailAdapter.getItems(),
indexOfFragment);
}
}
try {
if(!presenter.getImageQuality(indexOfFragment, inAppPictureLocation, requireActivity())) {
ActivityUtils.startActivityWithFlags(
getActivity(), MainActivity.class, Intent.FLAG_ACTIVITY_CLEAR_TOP,
Intent.FLAG_ACTIVITY_SINGLE_TOP);
}
} catch (Exception e) {
}
}
private void initializeFragment() {
if (binding == null) {
return;
}
binding.tvTitle.setText(getString(R.string.step_count, (indexOfFragment + 1),
callback.getTotalNumberOfSteps(), getString(R.string.media_detail_step_title)));
binding.tooltip.setOnClickListener(
v -> showInfoAlert(R.string.media_detail_step_title, R.string.media_details_tooltip));
initPresenter();
presenter.receiveImage(uploadableFile, place, inAppPictureLocation);
initRecyclerView();
if (indexOfFragment == 0) {
binding.btnPrevious.setEnabled(false);
binding.btnPrevious.setAlpha(0.5f);
} else {
binding.btnPrevious.setEnabled(true);
binding.btnPrevious.setAlpha(1.0f);
}
// If the image EXIF data contains the location, show the map icon with a green tick
if (inAppPictureLocation != null ||
(uploadableFile != null && uploadableFile.hasLocation())) {
Drawable mapTick = getResources().getDrawable(R.drawable.ic_map_available_20dp);
binding.locationImageView.setImageDrawable(mapTick);
binding.locationTextView.setText(R.string.edit_location);
} else {
// Otherwise, show the map icon with a red question mark
Drawable mapQuestionMark =
getResources().getDrawable(R.drawable.ic_map_not_available_20dp);
binding.locationImageView.setImageDrawable(mapQuestionMark);
binding.locationTextView.setText(R.string.add_location);
}
//If this is the last media, we have nothing to copy, lets not show the button
if (indexOfFragment == callback.getTotalNumberOfSteps() - 4) {
binding.btnCopySubsequentMedia.setVisibility(View.GONE);
} else {
binding.btnCopySubsequentMedia.setVisibility(View.VISIBLE);
}
binding.btnNext.setOnClickListener(v -> onNextButtonClicked());
binding.btnPrevious.setOnClickListener(v -> onPreviousButtonClicked());
binding.llEditImage.setOnClickListener(v -> onEditButtonClicked());
binding.llContainerTitle.setOnClickListener(v -> onLlContainerTitleClicked());
binding.llLocationStatus.setOnClickListener(v -> onIbMapClicked());
binding.btnCopySubsequentMedia.setOnClickListener(v -> onButtonCopyTitleDescToSubsequentMedia());
attachImageViewScaleChangeListener();
}
/**
* Attaches the scale change listener to the image view
*/
private void attachImageViewScaleChangeListener() {
binding.backgroundImage.setOnScaleChangeListener(
(scaleFactor, focusX, focusY) -> {
//Whenever the uses plays with the image, lets collapse the media detail container
//only if it is not already collapsed, which resolves flickering of arrow
if (isExpanded) {
expandCollapseLlMediaDetail(false);
}
});
}
/**
* attach the presenter with the view
*/
private void initPresenter() {
presenter.onAttachView(this);
}
/**
* init the description recycler veiw and caption recyclerview
*/
private void initRecyclerView() {
uploadMediaDetailAdapter = new UploadMediaDetailAdapter(this,
defaultKvStore.getString(Prefs.DESCRIPTION_LANGUAGE, ""), recentLanguagesDao, voiceInputResultLauncher);
uploadMediaDetailAdapter.setCallback(this::showInfoAlert);
uploadMediaDetailAdapter.setEventListener(this);
binding.rvDescriptions.setLayoutManager(new LinearLayoutManager(getContext()));
binding.rvDescriptions.setAdapter(uploadMediaDetailAdapter);
}
/**
* show dialog with info
* @param titleStringID
* @param messageStringId
*/
private void showInfoAlert(int titleStringID, int messageStringId) {
DialogUtil.showAlertDialog(getActivity(), getString(titleStringID),
getString(messageStringId), getString(android.R.string.ok), null);
}
public void onNextButtonClicked() {
if (callback == null) {
return;
}
presenter.displayLocDialog(indexOfFragment, inAppPictureLocation, hasUserRemovedLocation);
}
public void onPreviousButtonClicked() {
if (callback == null) {
return;
}
callback.onPreviousButtonClicked(indexOfFragment);
}
public void onEditButtonClicked() {
presenter.onEditButtonClicked(indexOfFragment);
}
@Override
public void showSimilarImageFragment(String originalFilePath, String possibleFilePath,
ImageCoordinates similarImageCoordinates) {
BasicKvStore basicKvStore = new BasicKvStore(getActivity(), "IsAnyImageCancelled");
if (!basicKvStore.getBoolean("IsAnyImageCancelled", false)) {
SimilarImageDialogFragment newFragment = new SimilarImageDialogFragment();
newFragment.setCancelable(false);
newFragment.setCallback(new SimilarImageDialogFragment.Callback() {
@Override
public void onPositiveResponse() {
Timber.d("positive response from similar image fragment");
presenter.useSimilarPictureCoordinates(similarImageCoordinates,
indexOfFragment);
// set the description text when user selects to use coordinate from the other image
// which was taken within 120s
// fixing: https://github.com/commons-app/apps-android-commons/issues/4700
uploadMediaDetailAdapter.getItems().get(0).setDescriptionText(
getString(R.string.similar_coordinate_description_auto_set));
updateMediaDetails(uploadMediaDetailAdapter.getItems());
// Replace the 'Add location' button with 'Edit location' button when user clicks
// yes in similar image dialog
// fixing: https://github.com/commons-app/apps-android-commons/issues/5669
Drawable mapTick = getResources().getDrawable(R.drawable.ic_map_available_20dp);
binding.locationImageView.setImageDrawable(mapTick);
binding.locationTextView.setText(R.string.edit_location);
}
@Override
public void onNegativeResponse() {
Timber.d("negative response from similar image fragment");
}
});
Bundle args = new Bundle();
args.putString("originalImagePath", originalFilePath);
args.putString("possibleImagePath", possibleFilePath);
newFragment.setArguments(args);
newFragment.show(getChildFragmentManager(), "dialog");
}
}
@Override
public void onImageProcessed(@NotNull UploadItem uploadItem) {
if (binding == null) {
return;
}
binding.backgroundImage.setImageURI(uploadItem.getMediaUri());
}
/**
* Sets variables to Show popup if any nearby location needing pictures matches uploadable picture's GPS location
* @param uploadItem
* @param place
*/
@Override
public void onNearbyPlaceFound(
@NotNull UploadItem uploadItem, @org.jetbrains.annotations.Nullable Place place) {
nearbyPlace = place;
this.uploadItem = uploadItem;
showNearbyFound = true;
if (callback == null) {
return;
}
if (indexOfFragment == 0) {
if (UploadActivity.nearbyPopupAnswers.containsKey(nearbyPlace)) {
final boolean response = UploadActivity.nearbyPopupAnswers.get(nearbyPlace);
if (response) {
if (callback != null) {
presenter.onUserConfirmedUploadIsOfPlace(nearbyPlace, indexOfFragment);
}
}
} else {
showNearbyPlaceFound(nearbyPlace);
}
showNearbyFound = false;
}
}
/**
* Shows nearby place found popup
* @param place
*/
@SuppressLint("StringFormatInvalid")
// To avoid the unwanted lint warning that string 'upload_nearby_place_found_description' is not of a valid format
private void showNearbyPlaceFound(Place place) {
final View customLayout = getLayoutInflater().inflate(R.layout.custom_nearby_found, null);
ImageView nearbyFoundImage = customLayout.findViewById(R.id.nearbyItemImage);
nearbyFoundImage.setImageURI(uploadItem.getMediaUri());
final Activity activity = getActivity();
if (activity instanceof UploadActivity) {
final boolean isMultipleFilesSelected = ((UploadActivity) activity).isMultipleFilesSelected();
// Determine the message based on the selection status
String message;
if (isMultipleFilesSelected) {
// Use plural message if multiple files are selected
message = String.format(Locale.getDefault(),
getString(R.string.upload_nearby_place_found_description_plural),
place.getName());
} else {
// Use singular message if only one file is selected
message = String.format(Locale.getDefault(),
getString(R.string.upload_nearby_place_found_description_singular),
place.getName());
}
// Show the AlertDialog with the determined message
DialogUtil.showAlertDialog(getActivity(),
getString(R.string.upload_nearby_place_found_title),
message,
() -> {
// Execute when user confirms the upload is of the specified place
UploadActivity.nearbyPopupAnswers.put(place, true);
presenter.onUserConfirmedUploadIsOfPlace(place, indexOfFragment);
},
() -> {
// Execute when user cancels the upload of the specified place
UploadActivity.nearbyPopupAnswers.put(place, false);
},
customLayout
);
}
}
@Override
public void showProgress(boolean shouldShow) {
if (callback == null) {
return;
}
callback.showProgress(shouldShow);
}
@Override
public void onImageValidationSuccess() {
if (callback == null) {
return;
}
callback.onNextButtonClicked(indexOfFragment);
}
/**
* This method gets called whenever the next/previous button is pressed
*/
@Override
public void onBecameVisible() {
super.onBecameVisible();
if (callback == null) {
return;
}
presenter.fetchTitleAndDescription(indexOfFragment);
if (showNearbyFound) {
if (UploadActivity.nearbyPopupAnswers.containsKey(nearbyPlace)) {
final boolean response = UploadActivity.nearbyPopupAnswers.get(nearbyPlace);
if (response) {
presenter.onUserConfirmedUploadIsOfPlace(nearbyPlace, indexOfFragment);
}
} else {
showNearbyPlaceFound(nearbyPlace);
}
showNearbyFound = false;
}
}
@Override
public void showMessage(int stringResourceId, int colorResourceId) {
ViewUtil.showLongToast(getContext(), stringResourceId);
}
@Override
public void showMessage(String message, int colorResourceId) {
ViewUtil.showLongToast(getContext(), message);
}
@Override
public void showDuplicatePicturePopup(@NotNull UploadItem uploadItem) {
if (defaultKvStore.getBoolean("showDuplicatePicturePopup", true)) {
String uploadTitleFormat = getString(R.string.upload_title_duplicate);
View checkBoxView = View
.inflate(getActivity(), R.layout.nearby_permission_dialog, null);
CheckBox checkBox = (CheckBox) checkBoxView.findViewById(R.id.never_ask_again);
checkBox.setOnCheckedChangeListener((buttonView, isChecked) -> {
if (isChecked) {
defaultKvStore.putBoolean("showDuplicatePicturePopup", false);
}
});
DialogUtil.showAlertDialog(requireActivity(),
getString(R.string.duplicate_file_name),
String.format(Locale.getDefault(),
uploadTitleFormat,
uploadItem.getFilename()),
getString(R.string.upload),
getString(R.string.cancel),
() -> {
uploadItem.setImageQuality(ImageUtils.IMAGE_KEEP);
onImageValidationSuccess();
}, null,
checkBoxView);
} else {
uploadItem.setImageQuality(ImageUtils.IMAGE_KEEP);
onImageValidationSuccess();
}
}
/**
* Shows a dialog alerting the user that internet connection is required for upload process
* Does nothing if there is network connectivity and then the user presses okay
*/
@Override
public void showConnectionErrorPopupForCaptionCheck() {
DialogUtil.showAlertDialog(getActivity(),
getString(R.string.upload_connection_error_alert_title),
getString(R.string.upload_connection_error_alert_detail),
getString(R.string.ok),
getString(R.string.cancel_upload),
() -> {
if (!NetworkUtils.isInternetConnectionEstablished(activity)) {
showConnectionErrorPopupForCaptionCheck();
}
},
() -> {
activity.finish();
});
}
/**
* Shows a dialog alerting the user that internet connection is required for upload process
* Recalls UploadMediaPresenter.getImageQuality for all the next upload items,
* if there is network connectivity and then the user presses okay
*/
@Override
public void showConnectionErrorPopup() {
try {
boolean FLAG_ALERT_DIALOG_SHOWING = basicKvStore.getBoolean(
keyForShowingAlertDialog, false);
if (!FLAG_ALERT_DIALOG_SHOWING) {
basicKvStore.putBoolean(keyForShowingAlertDialog, true);
DialogUtil.showAlertDialog(getActivity(),
getString(R.string.upload_connection_error_alert_title),
getString(R.string.upload_connection_error_alert_detail),
getString(R.string.ok),
getString(R.string.cancel_upload),
() -> {
basicKvStore.putBoolean(keyForShowingAlertDialog, false);
if (NetworkUtils.isInternetConnectionEstablished(activity)) {
int sizeOfUploads = basicKvStore.getInt(
UploadActivity.keyForCurrentUploadImagesSize);
for (int i = indexOfFragment; i < sizeOfUploads; i++) {
presenter.getImageQuality(i, inAppPictureLocation, activity);
}
} else {
showConnectionErrorPopup();
}
},
() -> {
basicKvStore.putBoolean(keyForShowingAlertDialog, false);
activity.finish();
},
null
);
}
} catch (Exception e) {
}
}
@Override
public void showExternalMap(@NotNull final UploadItem uploadItem) {
goToLocationPickerActivity(uploadItem);
}
/**
* Launches the image editing activity to edit the specified UploadItem.
*
* @param uploadItem The UploadItem to be edited.
*
* This method is called to start the image editing activity for a specific UploadItem.
* It sets the UploadItem as the currently editable item, creates an intent to launch the
* EditActivity, and passes the image file path as an extra in the intent. The activity
* is started using resultLauncher that handles the result in respective callback.
*/
@Override
public void showEditActivity(@NotNull UploadItem uploadItem) {
editableUploadItem = uploadItem;
Intent intent = new Intent(getContext(), EditActivity.class);
intent.putExtra("image", uploadableFile.getFilePath().toString());
startForEditActivityResult.launch(intent);
}
/**
* Start Location picker activity. Show the location first then user can modify it by clicking
* modify location button.
* @param uploadItem current upload item
*/
private void goToLocationPickerActivity(final UploadItem uploadItem) {
editableUploadItem = uploadItem;
double defaultLatitude = 37.773972;
double defaultLongitude = -122.431297;
double defaultZoom = 16.0;
final Intent locationPickerIntent;
/* Retrieve image location from EXIF if present or
check if user has provided location while using the in-app camera.
Use location of last UploadItem if none of them is available */
if (uploadItem.getGpsCoords() != null && uploadItem.getGpsCoords()
.getDecLatitude() != 0.0 && uploadItem.getGpsCoords().getDecLongitude() != 0.0) {
defaultLatitude = uploadItem.getGpsCoords()
.getDecLatitude();
defaultLongitude = uploadItem.getGpsCoords().getDecLongitude();
defaultZoom = uploadItem.getGpsCoords().getZoomLevel();
locationPickerIntent = new LocationPicker.IntentBuilder()
.defaultLocation(new CameraPosition(defaultLatitude,defaultLongitude,defaultZoom))
.activityKey("UploadActivity")
.build(getActivity());
} else {
if (defaultKvStore.getString(LAST_LOCATION) != null) {
final String[] locationLatLng
= defaultKvStore.getString(LAST_LOCATION).split(",");
defaultLatitude = Double.parseDouble(locationLatLng[0]);
defaultLongitude = Double.parseDouble(locationLatLng[1]);
}
if (defaultKvStore.getString(LAST_ZOOM) != null) {
defaultZoom = Double.parseDouble(defaultKvStore.getString(LAST_ZOOM));
}
locationPickerIntent = new LocationPicker.IntentBuilder()
.defaultLocation(new CameraPosition(defaultLatitude,defaultLongitude,defaultZoom))
.activityKey("NoLocationUploadActivity")
.build(getActivity());
}
startForResult.launch(locationPickerIntent);
}
private void onCameraPosition(ActivityResult result){
if (result.getResultCode() == RESULT_OK) {
assert result.getData() != null;
final CameraPosition cameraPosition = LocationPicker.getCameraPosition(result.getData());
if (cameraPosition != null) {
final String latitude = String.valueOf(cameraPosition.getLatitude());
final String longitude = String.valueOf(cameraPosition.getLongitude());
final double zoom = cameraPosition.getZoom();
editLocation(latitude, longitude, zoom);
// If isMissingLocationDialog is true, it means that the user has already tapped the
// "Next" button, so go directly to the next step.
if (isMissingLocationDialog) {
isMissingLocationDialog = false;
onNextButtonClicked();
}
} else {
// If camera position is null means location is removed by the user
removeLocation();
}
}
}
private void onVoiceInput(ActivityResult result) {
if (result.getResultCode() == RESULT_OK && result.getData() != null) {
ArrayList<String> resultData = result.getData().getStringArrayListExtra(
RecognizerIntent.EXTRA_RESULTS);
uploadMediaDetailAdapter.handleSpeechResult(resultData.get(0));
}else {
Timber.e("Error %s", result.getResultCode());
}
}
private void onEditActivityResult(ActivityResult result){
if (result.getResultCode() == RESULT_OK) {
String path = result.getData().getStringExtra("editedImageFilePath");
if (Objects.equals(result, "Error")) {
Timber.e("Error in rotating image");
return;
}
try {
if (binding != null){
binding.backgroundImage.setImageURI(Uri.fromFile(new File(path)));
}
editableUploadItem.setContentAndMediaUri(Uri.fromFile(new File(path)));
callback.changeThumbnail(indexOfFragment,
path);
} catch (Exception e) {
Timber.e(e);
}
}
}
/**
* Removes the location data from the image, by setting them to null
*/
public void removeLocation() {
editableUploadItem.getGpsCoords().setDecimalCoords(null);
try {
ExifInterface sourceExif = new ExifInterface(uploadableFile.getFilePath());
String[] exifTags = {
ExifInterface.TAG_GPS_LATITUDE,
ExifInterface.TAG_GPS_LATITUDE_REF,
ExifInterface.TAG_GPS_LONGITUDE,
ExifInterface.TAG_GPS_LONGITUDE_REF,
};
for (String tag : exifTags) {
sourceExif.setAttribute(tag, null);
}
sourceExif.saveAttributes();
Drawable mapQuestion = getResources().getDrawable(R.drawable.ic_map_not_available_20dp);
if (binding != null) {
binding.locationImageView.setImageDrawable(mapQuestion);
binding.locationTextView.setText(R.string.add_location);
}
editableUploadItem.getGpsCoords().setDecLatitude(0.0);
editableUploadItem.getGpsCoords().setDecLongitude(0.0);
editableUploadItem.getGpsCoords().setImageCoordsExists(false);
hasUserRemovedLocation = true;
Toast.makeText(getContext(), getString(R.string.location_removed), Toast.LENGTH_LONG)
.show();
} catch (Exception e) {
Timber.d(e);
Toast.makeText(getContext(), "Location could not be removed due to internal error",
Toast.LENGTH_LONG).show();
}
}
/**
* Update the old coordinates with new one
* @param latitude new latitude
* @param longitude new longitude
*/
public void editLocation(final String latitude, final String longitude, final double zoom) {
editableUploadItem.getGpsCoords().setDecLatitude(Double.parseDouble(latitude));
editableUploadItem.getGpsCoords().setDecLongitude(Double.parseDouble(longitude));
editableUploadItem.getGpsCoords().setDecimalCoords(latitude + "|" + longitude);
editableUploadItem.getGpsCoords().setImageCoordsExists(true);
editableUploadItem.getGpsCoords().setZoomLevel(zoom);
// Replace the map icon using the one with a green tick
Drawable mapTick = getResources().getDrawable(R.drawable.ic_map_available_20dp);
if (binding != null) {
binding.locationImageView.setImageDrawable(mapTick);
binding.locationTextView.setText(R.string.edit_location);
}
Toast.makeText(getContext(), getString(R.string.location_updated), Toast.LENGTH_LONG).show();
}
@Override
public void updateMediaDetails(@NotNull List<UploadMediaDetail> uploadMediaDetails) {
uploadMediaDetailAdapter.setItems(uploadMediaDetails);
showNearbyFound =
showNearbyFound && (
uploadMediaDetails == null || uploadMediaDetails.isEmpty()
|| listContainsEmptyDetails(
uploadMediaDetails));
}
/**
* if the media details that come in here are empty
* (empty caption AND empty description, with caption being the decider here)
* this method allows usage of nearby place caption and description if any
* else it takes the media details saved in prior for this picture
* @param uploadMediaDetails saved media details,
* ex: in case when "copy to subsequent media" button is clicked
* for a previous image
* @return boolean whether the details are empty or not
*/
private boolean listContainsEmptyDetails(List<UploadMediaDetail> uploadMediaDetails) {
for (UploadMediaDetail uploadDetail: uploadMediaDetails) {
if (!TextUtils.isEmpty(uploadDetail.getCaptionText()) && !TextUtils.isEmpty(uploadDetail.getDescriptionText())) {
return false;
}
}
return true;
}
/**
* Showing dialog for adding location
*
* @param onSkipClicked proceed for verifying image quality
*/
@Override
public void displayAddLocationDialog(@NotNull final Runnable onSkipClicked) {
isMissingLocationDialog = true;
DialogUtil.showAlertDialog(requireActivity(),
getString(R.string.no_location_found_title),
getString(R.string.no_location_found_message),
getString(R.string.add_location),
getString(R.string.skip_login),
this::onIbMapClicked,
onSkipClicked);
}
@Override
public void onDestroyView() {
super.onDestroyView();
presenter.onDetachView();
}
public void onLlContainerTitleClicked() {
expandCollapseLlMediaDetail(!isExpanded);
}
/**
* show hide media detail based on
* @param shouldExpand
*/
private void expandCollapseLlMediaDetail(boolean shouldExpand){
if (binding == null) {
return;
}
binding.llContainerMediaDetail.setVisibility(shouldExpand ? View.VISIBLE : View.GONE);
isExpanded = !isExpanded;
binding.ibExpandCollapse.setRotation(binding.ibExpandCollapse.getRotation() + 180);
}
public void onIbMapClicked() {
if (callback == null) {
return;
}
presenter.onMapIconClicked(indexOfFragment);
}
@Override
public void onPrimaryCaptionTextChange(boolean isNotEmpty) {
if (binding == null) {
return;
}
binding.btnCopySubsequentMedia.setEnabled(isNotEmpty);
binding.btnCopySubsequentMedia.setClickable(isNotEmpty);
binding.btnCopySubsequentMedia.setAlpha(isNotEmpty ? 1.0f : 0.5f);
binding.btnNext.setEnabled(isNotEmpty);
binding.btnNext.setClickable(isNotEmpty);
binding.btnNext.setAlpha(isNotEmpty ? 1.0f : 0.5f);
}
/**
* Adds new language item to RecyclerView
*/
@Override
public void addLanguage() {
UploadMediaDetail uploadMediaDetail = new UploadMediaDetail();
uploadMediaDetail.setManuallyAdded(true);//This was manually added by the user
uploadMediaDetailAdapter.addDescription(uploadMediaDetail);
binding.rvDescriptions.smoothScrollToPosition(uploadMediaDetailAdapter.getItemCount()-1);
}
public interface UploadMediaDetailFragmentCallback extends Callback {
void deletePictureAtIndex(int index);
void changeThumbnail(int index, String uri);
}
public void onButtonCopyTitleDescToSubsequentMedia(){
presenter.copyTitleAndDescriptionToSubsequentMedia(indexOfFragment);
Toast.makeText(getContext(), getResources().getString(R.string.copied_successfully), Toast.LENGTH_SHORT).show();
}
@Override
public void onSaveInstanceState(final Bundle outState) {
super.onSaveInstanceState(outState);
if(uploadableFile!=null){
outState.putParcelable(UPLOADABLE_FILE,uploadableFile);
}
if(uploadMediaDetailAdapter!=null){
outState.putParcelableArrayList(UPLOAD_MEDIA_DETAILS,
(ArrayList<? extends Parcelable>) uploadMediaDetailAdapter.getItems());
}
}
@Override
public void onDestroy() {
super.onDestroy();
binding = null;
}
}

View file

@ -0,0 +1,904 @@
package fr.free.nrw.commons.upload.mediaDetails
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.speech.RecognizerIntent
import android.text.TextUtils
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.CheckBox
import android.widget.CompoundButton
import android.widget.ImageView
import android.widget.Toast
import androidx.activity.result.ActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import androidx.core.os.bundleOf
import androidx.exifinterface.media.ExifInterface
import androidx.recyclerview.widget.LinearLayoutManager
import fr.free.nrw.commons.CameraPosition
import fr.free.nrw.commons.R
import fr.free.nrw.commons.contributions.MainActivity
import fr.free.nrw.commons.databinding.FragmentUploadMediaDetailFragmentBinding
import fr.free.nrw.commons.edit.EditActivity
import fr.free.nrw.commons.filepicker.UploadableFile
import fr.free.nrw.commons.kvstore.BasicKvStore
import fr.free.nrw.commons.kvstore.JsonKvStore
import fr.free.nrw.commons.location.LatLng
import fr.free.nrw.commons.locationpicker.LocationPicker
import fr.free.nrw.commons.locationpicker.LocationPicker.getCameraPosition
import fr.free.nrw.commons.nearby.Place
import fr.free.nrw.commons.recentlanguages.RecentLanguagesDao
import fr.free.nrw.commons.settings.Prefs
import fr.free.nrw.commons.upload.ImageCoordinates
import fr.free.nrw.commons.upload.SimilarImageDialogFragment
import fr.free.nrw.commons.upload.UploadActivity
import fr.free.nrw.commons.upload.UploadBaseFragment
import fr.free.nrw.commons.upload.UploadItem
import fr.free.nrw.commons.upload.UploadMediaDetail
import fr.free.nrw.commons.upload.UploadMediaDetailAdapter
import fr.free.nrw.commons.upload.mediaDetails.UploadMediaPresenter.Companion.presenterCallback
import fr.free.nrw.commons.utils.ActivityUtils.startActivityWithFlags
import fr.free.nrw.commons.utils.DialogUtil.showAlertDialog
import fr.free.nrw.commons.utils.ImageUtils
import fr.free.nrw.commons.utils.ImageUtils.IMAGE_OK
import fr.free.nrw.commons.utils.ImageUtils.getErrorMessageForResult
import fr.free.nrw.commons.utils.NetworkUtils.isInternetConnectionEstablished
import fr.free.nrw.commons.utils.ViewUtil.showLongToast
import timber.log.Timber
import java.io.File
import java.util.ArrayList
import java.util.Locale
import java.util.Objects
import javax.inject.Inject
import javax.inject.Named
class UploadMediaDetailFragment : UploadBaseFragment(), UploadMediaDetailsContract.View,
UploadMediaDetailAdapter.EventListener {
private val startForResult = registerForActivityResult<Intent, ActivityResult>(
ActivityResultContracts.StartActivityForResult(), ::onCameraPosition)
private val startForEditActivityResult = registerForActivityResult<Intent, ActivityResult>(
ActivityResultContracts.StartActivityForResult(), ::onEditActivityResult)
private val voiceInputResultLauncher = registerForActivityResult<Intent, ActivityResult>(
ActivityResultContracts.StartActivityForResult(), ::onVoiceInput)
@Inject
lateinit var presenter: UploadMediaDetailsContract.UserActionListener
@Inject
@field:Named("default_preferences")
lateinit var defaultKvStore: JsonKvStore
@Inject
lateinit var recentLanguagesDao: RecentLanguagesDao
/**
* True when user removes location from the current image
*/
var hasUserRemovedLocation = false
/**
* True if location is added via the "missing location" popup dialog (which appears after
* tapping "Next" if the picture has no geographical coordinates).
*/
private var isMissingLocationDialog = false
/**
* showNearbyFound will be true, if any nearby location found that needs pictures and the nearby
* popup is yet to be shown Used to show and check if the nearby found popup is already shown
*/
private var showNearbyFound = false
/**
* nearbyPlace holds the detail of nearby place that need pictures, if any found
*/
private var nearbyPlace: Place? = null
private var uploadItem: UploadItem? = null
/**
* inAppPictureLocation: use location recorded while using the in-app camera if device camera
* does not record it in the EXIF
*/
var inAppPictureLocation: LatLng? = null
/**
* editableUploadItem : Storing the upload item before going to update the coordinates
*/
private var editableUploadItem: UploadItem? = null
private var _binding: FragmentUploadMediaDetailFragmentBinding? = null
private val binding: FragmentUploadMediaDetailFragmentBinding get() = _binding!!
private var basicKvStore: BasicKvStore? = null
private val keyForShowingAlertDialog = "isNoNetworkAlertDialogShowing"
private var uploadableFile: UploadableFile? = null
private var place: Place? = null
private lateinit var uploadMediaDetailAdapter: UploadMediaDetailAdapter
var indexOfFragment = 0
var isExpanded = true
var fragmentCallback: UploadMediaDetailFragmentCallback? = null
set(value) {
field = value
UploadMediaPresenter.presenterCallback = value
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (savedInstanceState != null && uploadableFile == null) {
uploadableFile = savedInstanceState.getParcelable(UPLOADABLE_FILE)
}
}
fun setImageToBeUploaded(
uploadableFile: UploadableFile?, place: Place?, inAppPictureLocation: LatLng?
) {
this.uploadableFile = uploadableFile
this.place = place
this.inAppPictureLocation = inAppPictureLocation
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
): View {
_binding = FragmentUploadMediaDetailFragmentBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
basicKvStore = BasicKvStore(requireActivity(), "CurrentUploadImageQualities")
if (fragmentCallback != null) {
indexOfFragment = fragmentCallback!!.getIndexInViewFlipper(this)
initializeFragment()
}
if (savedInstanceState != null) {
if (uploadMediaDetailAdapter.items.isEmpty() && fragmentCallback != null) {
uploadMediaDetailAdapter.items = savedInstanceState.getParcelableArrayList(UPLOAD_MEDIA_DETAILS)!!
presenter.setUploadMediaDetails(uploadMediaDetailAdapter.items, indexOfFragment)
}
}
try {
if (!presenter.getImageQuality(indexOfFragment, inAppPictureLocation, requireActivity())) {
startActivityWithFlags(
requireActivity(),
MainActivity::class.java,
Intent.FLAG_ACTIVITY_CLEAR_TOP,
Intent.FLAG_ACTIVITY_SINGLE_TOP
)
}
} catch (_: Exception) {
}
}
private fun initializeFragment() {
if (_binding == null) {
return
}
binding.tvTitle.text = getString(
R.string.step_count, (indexOfFragment + 1),
fragmentCallback!!.totalNumberOfSteps, getString(R.string.media_detail_step_title)
)
binding.tooltip.setOnClickListener {
showInfoAlert(
R.string.media_detail_step_title,
R.string.media_details_tooltip
)
}
presenter.onAttachView(this)
presenter.receiveImage(uploadableFile, place, inAppPictureLocation)
initRecyclerView()
with (binding){
if (indexOfFragment == 0) {
btnPrevious.isEnabled = false
btnPrevious.alpha = 0.5f
} else {
btnPrevious.isEnabled = true
btnPrevious.alpha = 1.0f
}
// If the image EXIF data contains the location, show the map icon with a green tick
if (inAppPictureLocation != null || (uploadableFile != null && uploadableFile!!.hasLocation())) {
val mapTick =
ContextCompat.getDrawable(requireContext(), R.drawable.ic_map_available_20dp)
locationImageView.setImageDrawable(mapTick)
locationTextView.setText(R.string.edit_location)
} else {
// Otherwise, show the map icon with a red question mark
val mapQuestionMark = ContextCompat.getDrawable(
requireContext(),
R.drawable.ic_map_not_available_20dp
)
locationImageView.setImageDrawable(mapQuestionMark)
locationTextView.setText(R.string.add_location)
}
//If this is the last media, we have nothing to copy, lets not show the button
btnCopySubsequentMedia.visibility =
if (indexOfFragment == fragmentCallback!!.totalNumberOfSteps - 4) {
View.GONE
} else {
View.VISIBLE
}
btnNext.setOnClickListener { presenter.displayLocDialog(indexOfFragment, inAppPictureLocation, hasUserRemovedLocation) }
btnPrevious.setOnClickListener { fragmentCallback?.onPreviousButtonClicked(indexOfFragment) }
llEditImage.setOnClickListener { presenter.onEditButtonClicked(indexOfFragment) }
llContainerTitle.setOnClickListener { expandCollapseLlMediaDetail(!isExpanded) }
llLocationStatus.setOnClickListener { presenter.onMapIconClicked(indexOfFragment) }
btnCopySubsequentMedia.setOnClickListener { onButtonCopyTitleDescToSubsequentMedia() }
}
attachImageViewScaleChangeListener()
}
/**
* Attaches the scale change listener to the image view
*/
private fun attachImageViewScaleChangeListener() {
binding.backgroundImage.setOnScaleChangeListener { _: Float, _: Float, _: Float ->
//Whenever the uses plays with the image, lets collapse the media detail container
//only if it is not already collapsed, which resolves flickering of arrow
if (isExpanded) {
expandCollapseLlMediaDetail(false)
}
}
}
/**
* init the description recycler veiw and caption recyclerview
*/
private fun initRecyclerView() {
uploadMediaDetailAdapter = UploadMediaDetailAdapter(
this,
defaultKvStore.getString(Prefs.DESCRIPTION_LANGUAGE, "")!!,
recentLanguagesDao, voiceInputResultLauncher
)
uploadMediaDetailAdapter.callback =
UploadMediaDetailAdapter.Callback { titleStringID: Int, messageStringId: Int ->
showInfoAlert(titleStringID, messageStringId)
}
uploadMediaDetailAdapter.eventListener = this
binding.rvDescriptions.layoutManager = LinearLayoutManager(context)
binding.rvDescriptions.adapter = uploadMediaDetailAdapter
}
private fun showInfoAlert(titleStringID: Int, messageStringId: Int) {
showAlertDialog(
requireActivity(),
getString(titleStringID),
getString(messageStringId),
getString(android.R.string.ok),
null
)
}
override fun showSimilarImageFragment(
originalFilePath: String?, possibleFilePath: String?,
similarImageCoordinates: ImageCoordinates?
) {
val basicKvStore = BasicKvStore(requireActivity(), "IsAnyImageCancelled")
if (!basicKvStore.getBoolean("IsAnyImageCancelled", false)) {
val newFragment = SimilarImageDialogFragment()
newFragment.isCancelable = false
newFragment.callback = object : SimilarImageDialogFragment.Callback {
override fun onPositiveResponse() {
Timber.d("positive response from similar image fragment")
presenter.useSimilarPictureCoordinates(
similarImageCoordinates!!,
indexOfFragment
)
// set the description text when user selects to use coordinate from the other image
// which was taken within 120s
// fixing: https://github.com/commons-app/apps-android-commons/issues/4700
uploadMediaDetailAdapter.items[0].descriptionText =
getString(R.string.similar_coordinate_description_auto_set)
updateMediaDetails(uploadMediaDetailAdapter.items)
// Replace the 'Add location' button with 'Edit location' button when user clicks
// yes in similar image dialog
// fixing: https://github.com/commons-app/apps-android-commons/issues/5669
val mapTick = ContextCompat.getDrawable(
requireContext(),
R.drawable.ic_map_available_20dp
)
binding.locationImageView.setImageDrawable(mapTick)
binding.locationTextView.setText(R.string.edit_location)
}
override fun onNegativeResponse() {
Timber.d("negative response from similar image fragment")
}
}
newFragment.arguments = bundleOf(
"originalImagePath" to originalFilePath,
"possibleImagePath" to possibleFilePath
)
newFragment.show(childFragmentManager, "dialog")
}
}
override fun onImageProcessed(uploadItem: UploadItem) {
if (_binding == null) {
return
}
binding.backgroundImage.setImageURI(uploadItem.mediaUri)
}
override fun onNearbyPlaceFound(
uploadItem: UploadItem, place: Place?
) {
nearbyPlace = place
this.uploadItem = uploadItem
showNearbyFound = true
if (fragmentCallback == null) {
return
}
if (indexOfFragment == 0) {
if (UploadActivity.nearbyPopupAnswers!!.containsKey(nearbyPlace!!)) {
val response = UploadActivity.nearbyPopupAnswers!![nearbyPlace!!]!!
if (response) {
if (fragmentCallback != null) {
presenter.onUserConfirmedUploadIsOfPlace(nearbyPlace, indexOfFragment)
}
}
} else {
showNearbyPlaceFound(nearbyPlace!!)
}
showNearbyFound = false
}
}
@SuppressLint("StringFormatInvalid") // To avoid the unwanted lint warning that string 'upload_nearby_place_found_description' is not of a valid format
private fun showNearbyPlaceFound(place: Place) {
val customLayout = layoutInflater.inflate(R.layout.custom_nearby_found, null)
val nearbyFoundImage = customLayout.findViewById<ImageView>(R.id.nearbyItemImage)
nearbyFoundImage.setImageURI(uploadItem!!.mediaUri)
val activity: Activity? = activity
if (activity is UploadActivity) {
val isMultipleFilesSelected = activity.isMultipleFilesSelected
// Determine the message based on the selection status
val message = if (isMultipleFilesSelected) {
// Use plural message if multiple files are selected
String.format(
Locale.getDefault(),
getString(R.string.upload_nearby_place_found_description_plural),
place.getName()
)
} else {
// Use singular message if only one file is selected
String.format(
Locale.getDefault(),
getString(R.string.upload_nearby_place_found_description_singular),
place.getName()
)
}
// Show the AlertDialog with the determined message
showAlertDialog(
requireActivity(),
getString(R.string.upload_nearby_place_found_title),
message,
{
// Execute when user confirms the upload is of the specified place
UploadActivity.nearbyPopupAnswers!![place] = true
presenter.onUserConfirmedUploadIsOfPlace(place, indexOfFragment)
},
{
// Execute when user cancels the upload of the specified place
UploadActivity.nearbyPopupAnswers!![place] = false
},
customLayout
)
}
}
override fun showProgress(shouldShow: Boolean) {
if (fragmentCallback == null) {
return
}
fragmentCallback!!.showProgress(shouldShow)
}
override fun onImageValidationSuccess() {
if (fragmentCallback == null) {
return
}
fragmentCallback!!.onNextButtonClicked(indexOfFragment)
}
/**
* This method gets called whenever the next/previous button is pressed
*/
override fun onBecameVisible() {
super.onBecameVisible()
if (fragmentCallback == null) {
return
}
presenter.fetchTitleAndDescription(indexOfFragment)
if (showNearbyFound) {
if (UploadActivity.nearbyPopupAnswers!!.containsKey(nearbyPlace!!)) {
val response = UploadActivity.nearbyPopupAnswers!![nearbyPlace!!]!!
if (response) {
presenter.onUserConfirmedUploadIsOfPlace(nearbyPlace, indexOfFragment)
}
} else {
showNearbyPlaceFound(nearbyPlace!!)
}
showNearbyFound = false
}
}
override fun showMessage(stringResourceId: Int, colorResourceId: Int) =
showLongToast(requireContext(), stringResourceId)
override fun showMessage(message: String, colorResourceId: Int) =
showLongToast(requireContext(), message)
override fun showDuplicatePicturePopup(uploadItem: UploadItem) {
if (defaultKvStore.getBoolean("showDuplicatePicturePopup", true)) {
val uploadTitleFormat = getString(R.string.upload_title_duplicate)
val checkBoxView = View
.inflate(activity, R.layout.nearby_permission_dialog, null)
val checkBox = checkBoxView.findViewById<View>(R.id.never_ask_again) as CheckBox
checkBox.setOnCheckedChangeListener { buttonView: CompoundButton?, isChecked: Boolean ->
if (isChecked) {
defaultKvStore.putBoolean("showDuplicatePicturePopup", false)
}
}
showAlertDialog(
requireActivity(),
getString(R.string.duplicate_file_name),
String.format(
Locale.getDefault(),
uploadTitleFormat,
uploadItem.filename
),
getString(R.string.upload),
getString(R.string.cancel),
{
uploadItem.imageQuality = ImageUtils.IMAGE_KEEP
onImageValidationSuccess()
}, null,
checkBoxView
)
} else {
uploadItem.imageQuality = ImageUtils.IMAGE_KEEP
onImageValidationSuccess()
}
}
/**
* Shows a dialog alerting the user that internet connection is required for upload process
* Does nothing if there is network connectivity and then the user presses okay
*/
override fun showConnectionErrorPopupForCaptionCheck() {
showAlertDialog(requireActivity(),
getString(R.string.upload_connection_error_alert_title),
getString(R.string.upload_connection_error_alert_detail),
getString(R.string.ok),
getString(R.string.cancel_upload),
{
if (!isInternetConnectionEstablished(requireActivity())) {
showConnectionErrorPopupForCaptionCheck()
}
},
{
requireActivity().finish()
})
}
/**
* Shows a dialog alerting the user that internet connection is required for upload process
* Recalls UploadMediaPresenter.getImageQuality for all the next upload items,
* if there is network connectivity and then the user presses okay
*/
override fun showConnectionErrorPopup() {
try {
val FLAG_ALERT_DIALOG_SHOWING = basicKvStore!!.getBoolean(
keyForShowingAlertDialog, false
)
if (!FLAG_ALERT_DIALOG_SHOWING) {
basicKvStore!!.putBoolean(keyForShowingAlertDialog, true)
showAlertDialog(
requireActivity(),
getString(R.string.upload_connection_error_alert_title),
getString(R.string.upload_connection_error_alert_detail),
getString(R.string.ok),
getString(R.string.cancel_upload),
{
basicKvStore!!.putBoolean(keyForShowingAlertDialog, false)
if (isInternetConnectionEstablished(requireActivity())) {
val sizeOfUploads = basicKvStore!!.getInt(
UploadActivity.keyForCurrentUploadImagesSize
)
for (i in indexOfFragment until sizeOfUploads) {
presenter.getImageQuality(
i,
inAppPictureLocation,
requireActivity()
)
}
} else {
showConnectionErrorPopup()
}
},
{
basicKvStore!!.putBoolean(keyForShowingAlertDialog, false)
requireActivity().finish()
},
null
)
}
} catch (e: Exception) {
Timber.e(e)
}
}
override fun showExternalMap(uploadItem: UploadItem) =
goToLocationPickerActivity(uploadItem)
/**
* Launches the image editing activity to edit the specified UploadItem.
*
* @param uploadItem The UploadItem to be edited.
*
* This method is called to start the image editing activity for a specific UploadItem.
* It sets the UploadItem as the currently editable item, creates an intent to launch the
* EditActivity, and passes the image file path as an extra in the intent. The activity
* is started using resultLauncher that handles the result in respective callback.
*/
override fun showEditActivity(uploadItem: UploadItem) {
editableUploadItem = uploadItem
val intent = Intent(context, EditActivity::class.java)
intent.putExtra("image", uploadableFile!!.getFilePath().toString())
startForEditActivityResult.launch(intent)
}
/**
* Start Location picker activity. Show the location first then user can modify it by clicking
* modify location button.
* @param uploadItem current upload item
*/
private fun goToLocationPickerActivity(uploadItem: UploadItem) {
editableUploadItem = uploadItem
var defaultLatitude = 37.773972
var defaultLongitude = -122.431297
var defaultZoom = 16.0
val locationPickerIntent: Intent
/* Retrieve image location from EXIF if present or
check if user has provided location while using the in-app camera.
Use location of last UploadItem if none of them is available */
if (uploadItem.gpsCoords != null && uploadItem.gpsCoords!!
.decLatitude != 0.0 && uploadItem.gpsCoords!!.decLongitude != 0.0
) {
defaultLatitude = uploadItem.gpsCoords!!
.decLatitude
defaultLongitude = uploadItem.gpsCoords!!.decLongitude
defaultZoom = uploadItem.gpsCoords!!.zoomLevel
locationPickerIntent = LocationPicker.IntentBuilder()
.defaultLocation(CameraPosition(defaultLatitude, defaultLongitude, defaultZoom))
.activityKey("UploadActivity")
.build(requireActivity())
} else {
if (defaultKvStore.getString(LAST_LOCATION) != null) {
val locationLatLng = defaultKvStore.getString(LAST_LOCATION)!!
.split(",".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
defaultLatitude = locationLatLng[0].toDouble()
defaultLongitude = locationLatLng[1].toDouble()
}
if (defaultKvStore.getString(LAST_ZOOM) != null) {
defaultZoom = defaultKvStore.getString(LAST_ZOOM)!!
.toDouble()
}
locationPickerIntent = LocationPicker.IntentBuilder()
.defaultLocation(CameraPosition(defaultLatitude, defaultLongitude, defaultZoom))
.activityKey("NoLocationUploadActivity")
.build(requireActivity())
}
startForResult.launch(locationPickerIntent)
}
private fun onCameraPosition(result: ActivityResult) {
if (result.resultCode == Activity.RESULT_OK) {
checkNotNull(result.data)
val cameraPosition = getCameraPosition(
result.data!!
)
if (cameraPosition != null) {
val latitude = cameraPosition.latitude.toString()
val longitude = cameraPosition.longitude.toString()
val zoom = cameraPosition.zoom
editLocation(latitude, longitude, zoom)
// If isMissingLocationDialog is true, it means that the user has already tapped the
// "Next" button, so go directly to the next step.
if (isMissingLocationDialog) {
isMissingLocationDialog = false
presenter.displayLocDialog(
indexOfFragment,
inAppPictureLocation,
hasUserRemovedLocation
)
}
} else {
// If camera position is null means location is removed by the user
removeLocation()
}
}
}
private fun onVoiceInput(result: ActivityResult) {
if (result.resultCode == Activity.RESULT_OK && result.data != null) {
val resultData = result.data!!.getStringArrayListExtra(
RecognizerIntent.EXTRA_RESULTS
)
uploadMediaDetailAdapter.handleSpeechResult(resultData!![0])
} else {
Timber.e("Error %s", result.resultCode)
}
}
private fun onEditActivityResult(result: ActivityResult) {
if (result.resultCode == Activity.RESULT_OK) {
val path = result.data!!.getStringExtra("editedImageFilePath")
if (Objects.equals(result, "Error")) {
Timber.e("Error in rotating image")
return
}
try {
if (_binding != null) {
binding.backgroundImage.setImageURI(Uri.fromFile(File(path!!)))
}
editableUploadItem!!.setContentAndMediaUri(Uri.fromFile(File(path!!)))
fragmentCallback!!.changeThumbnail(
indexOfFragment,
path
)
} catch (e: Exception) {
Timber.e(e)
}
}
}
/**
* Removes the location data from the image, by setting them to null
*/
private fun removeLocation() {
editableUploadItem!!.gpsCoords!!.decimalCoords = null
try {
val sourceExif = ExifInterface(
uploadableFile!!.getFilePath()
)
val exifTags = arrayOf(
ExifInterface.TAG_GPS_LATITUDE,
ExifInterface.TAG_GPS_LATITUDE_REF,
ExifInterface.TAG_GPS_LONGITUDE,
ExifInterface.TAG_GPS_LONGITUDE_REF,
)
for (tag in exifTags) {
sourceExif.setAttribute(tag, null)
}
sourceExif.saveAttributes()
val mapQuestion =
ContextCompat.getDrawable(requireContext(), R.drawable.ic_map_not_available_20dp)
if (_binding != null) {
binding.locationImageView.setImageDrawable(mapQuestion)
binding.locationTextView.setText(R.string.add_location)
}
editableUploadItem!!.gpsCoords!!.decLatitude = 0.0
editableUploadItem!!.gpsCoords!!.decLongitude = 0.0
editableUploadItem!!.gpsCoords!!.imageCoordsExists = false
hasUserRemovedLocation = true
Toast.makeText(context, getString(R.string.location_removed), Toast.LENGTH_LONG)
.show()
} catch (e: Exception) {
Timber.d(e)
Toast.makeText(
context, "Location could not be removed due to internal error",
Toast.LENGTH_LONG
).show()
}
}
/**
* Update the old coordinates with new one
* @param latitude new latitude
* @param longitude new longitude
*/
fun editLocation(latitude: String, longitude: String, zoom: Double) {
editableUploadItem!!.gpsCoords!!.decLatitude = latitude.toDouble()
editableUploadItem!!.gpsCoords!!.decLongitude = longitude.toDouble()
editableUploadItem!!.gpsCoords!!.decimalCoords = "$latitude|$longitude"
editableUploadItem!!.gpsCoords!!.imageCoordsExists = true
editableUploadItem!!.gpsCoords!!.zoomLevel = zoom
// Replace the map icon using the one with a green tick
val mapTick = ContextCompat.getDrawable(requireContext(), R.drawable.ic_map_available_20dp)
if (_binding != null) {
binding.locationImageView.setImageDrawable(mapTick)
binding.locationTextView.setText(R.string.edit_location)
}
Toast.makeText(context, getString(R.string.location_updated), Toast.LENGTH_LONG).show()
}
override fun updateMediaDetails(uploadMediaDetails: List<UploadMediaDetail>) {
uploadMediaDetailAdapter.items = uploadMediaDetails
showNearbyFound =
showNearbyFound && (uploadMediaDetails.isEmpty() || listContainsEmptyDetails(
uploadMediaDetails
))
}
/**
* if the media details that come in here are empty
* (empty caption AND empty description, with caption being the decider here)
* this method allows usage of nearby place caption and description if any
* else it takes the media details saved in prior for this picture
* @param uploadMediaDetails saved media details,
* ex: in case when "copy to subsequent media" button is clicked
* for a previous image
* @return boolean whether the details are empty or not
*/
private fun listContainsEmptyDetails(uploadMediaDetails: List<UploadMediaDetail>): Boolean {
for ((_, descriptionText, captionText) in uploadMediaDetails) {
if (!TextUtils.isEmpty(captionText) && !TextUtils.isEmpty(
descriptionText
)
) {
return false
}
}
return true
}
/**
* Showing dialog for adding location
*
* @param runnable proceed for verifying image quality
*/
override fun displayAddLocationDialog(runnable: Runnable) {
isMissingLocationDialog = true
showAlertDialog(
requireActivity(),
getString(R.string.no_location_found_title),
getString(R.string.no_location_found_message),
getString(R.string.add_location),
getString(R.string.skip_login),
{
presenter.onMapIconClicked(indexOfFragment)
},
runnable
)
}
override fun createBasicKvStore(storeName: String): BasicKvStore =
BasicKvStore(requireActivity(), storeName)
override fun showBadImagePopup(errorCode: Int, index: Int, uploadItem: UploadItem) {
//If the error message is null, we will probably not show anything
val activity = requireActivity()
val errorMessageForResult = getErrorMessageForResult(activity, errorCode)
if (errorMessageForResult.isNotEmpty()) {
showAlertDialog(
activity,
activity.getString(R.string.upload_problem_image),
errorMessageForResult,
activity.getString(R.string.upload),
activity.getString(R.string.cancel),
{
showProgress(false)
uploadItem.imageQuality = IMAGE_OK
},
{
presenterCallback!!.deletePictureAtIndex(index)
}
)?.setCancelable(false)
}
}
override fun onDestroyView() {
super.onDestroyView()
presenter.onDetachView()
}
fun expandCollapseLlMediaDetail(shouldExpand: Boolean) {
if (_binding == null) {
return
}
binding.llContainerMediaDetail.visibility =
if (shouldExpand) View.VISIBLE else View.GONE
isExpanded = !isExpanded
binding.ibExpandCollapse.rotation = binding.ibExpandCollapse.rotation + 180
}
override fun onPrimaryCaptionTextChange(isNotEmpty: Boolean) {
if (_binding == null) {
return
}
binding.btnCopySubsequentMedia.isEnabled = isNotEmpty
binding.btnCopySubsequentMedia.isClickable = isNotEmpty
binding.btnCopySubsequentMedia.alpha = if (isNotEmpty) 1.0f else 0.5f
binding.btnNext.isEnabled = isNotEmpty
binding.btnNext.isClickable = isNotEmpty
binding.btnNext.alpha = if (isNotEmpty) 1.0f else 0.5f
}
/**
* Adds new language item to RecyclerView
*/
override fun addLanguage() {
val uploadMediaDetail = UploadMediaDetail()
uploadMediaDetail.isManuallyAdded = true //This was manually added by the user
uploadMediaDetailAdapter.addDescription(uploadMediaDetail)
binding.rvDescriptions.smoothScrollToPosition(uploadMediaDetailAdapter.itemCount - 1)
}
fun onButtonCopyTitleDescToSubsequentMedia() {
presenter.copyTitleAndDescriptionToSubsequentMedia(indexOfFragment)
Toast.makeText(context, R.string.copied_successfully, Toast.LENGTH_SHORT).show()
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
if (uploadableFile != null) {
outState.putParcelable(UPLOADABLE_FILE, uploadableFile)
}
outState.putParcelableArrayList(
UPLOAD_MEDIA_DETAILS,
ArrayList(uploadMediaDetailAdapter.items)
)
}
override fun onDestroy() {
super.onDestroy()
_binding = null
}
interface UploadMediaDetailFragmentCallback : Callback {
fun deletePictureAtIndex(index: Int)
fun changeThumbnail(index: Int, uri: String)
}
companion object {
/**
* A key for applicationKvStore. By this key we can retrieve the location of last UploadItem ex.
* 12.3433,54.78897 from applicationKvStore.
*/
const val LAST_LOCATION: String = "last_location_while_uploading"
const val LAST_ZOOM: String = "last_zoom_level_while_uploading"
const val UPLOADABLE_FILE: String = "uploadable_file"
const val UPLOAD_MEDIA_DETAILS: String = "upload_media_detail_adapter"
}
}

View file

@ -3,6 +3,7 @@ package fr.free.nrw.commons.upload.mediaDetails
import android.app.Activity
import fr.free.nrw.commons.BasePresenter
import fr.free.nrw.commons.filepicker.UploadableFile
import fr.free.nrw.commons.kvstore.BasicKvStore
import fr.free.nrw.commons.location.LatLng
import fr.free.nrw.commons.nearby.Place
import fr.free.nrw.commons.upload.ImageCoordinates
@ -25,7 +26,7 @@ interface UploadMediaDetailsContract {
fun showMessage(stringResourceId: Int, colorResourceId: Int)
fun showMessage(message: String?, colorResourceId: Int)
fun showMessage(message: String, colorResourceId: Int)
fun showDuplicatePicturePopup(uploadItem: UploadItem)
@ -49,6 +50,10 @@ interface UploadMediaDetailsContract {
fun updateMediaDetails(uploadMediaDetails: List<UploadMediaDetail>)
fun displayAddLocationDialog(runnable: Runnable)
fun createBasicKvStore(storeName: String): BasicKvStore
fun showBadImagePopup(errorCode: Int, index: Int, uploadItem: UploadItem)
}
interface UserActionListener : BasePresenter<View?> {

View file

@ -336,8 +336,7 @@ class UploadMediaPresenter @Inject constructor(
*/
override fun checkImageQuality(uploadItem: UploadItem, index: Int) {
if ((uploadItem.imageQuality != IMAGE_OK) && (uploadItem.imageQuality != IMAGE_KEEP)) {
val store = BasicKvStore(
UploadMediaDetailFragment.activity,
val store = view.createBasicKvStore(
UploadActivity.storeNameForCurrentUploadImagesSize
)
val value = store.getString(UPLOAD_QUALITIES_KEY, null)
@ -363,8 +362,7 @@ class UploadMediaPresenter @Inject constructor(
* @param index Index of the UploadItem which was deleted
*/
override fun updateImageQualitiesJSON(size: Int, index: Int) {
val store = BasicKvStore(
UploadMediaDetailFragment.activity,
val store = view.createBasicKvStore(
UploadActivity.storeNameForCurrentUploadImagesSize
)
val value = store.getString(UPLOAD_QUALITIES_KEY, null)
@ -399,36 +397,7 @@ class UploadMediaPresenter @Inject constructor(
// If image has some other problems, show popup accordingly
if (errorCode != EMPTY_CAPTION && errorCode != FILE_NAME_EXISTS) {
showBadImagePopup(errorCode, index, UploadMediaDetailFragment.activity, uploadItem)
}
}
/**
* Shows a dialog describing the potential problems in the current image
*
* @param errorCode Has the potential problems in the current image
* @param index Index of the UploadItem which has problems
* @param activity Context reference
* @param uploadItem UploadItem which has problems
*/
private fun showBadImagePopup(
errorCode: Int, index: Int, activity: Activity, uploadItem: UploadItem
) {
//If the error message is null, we will probably not show anything
val errorMessageForResult = getErrorMessageForResult(activity, errorCode)
if (errorMessageForResult.isNotEmpty()) {
showAlertDialog(activity,
activity.getString(R.string.upload_problem_image),
errorMessageForResult,
activity.getString(R.string.upload),
activity.getString(R.string.cancel),
{
view.showProgress(false)
uploadItem.imageQuality = IMAGE_OK
}, {
presenterCallback!!.deletePictureAtIndex(index)
}
)?.setCancelable(false)
view.showBadImagePopup(errorCode, index, uploadItem)
}
}

View file

@ -13,8 +13,8 @@ import com.nhaarman.mockitokotlin2.verify
import fr.free.nrw.commons.CameraPosition
import fr.free.nrw.commons.TestCommonsApplication
import fr.free.nrw.commons.kvstore.JsonKvStore
import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment.LAST_LOCATION
import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment.LAST_ZOOM
import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment.Companion.LAST_LOCATION
import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment.Companion.LAST_ZOOM
import io.reactivex.android.plugins.RxAndroidPlugins
import io.reactivex.schedulers.Schedulers
import org.junit.Assert

View file

@ -34,7 +34,7 @@ import fr.free.nrw.commons.upload.ImageCoordinates
import fr.free.nrw.commons.upload.UploadActivity
import fr.free.nrw.commons.upload.UploadItem
import fr.free.nrw.commons.upload.UploadMediaDetailAdapter
import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment.LAST_ZOOM
import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment.Companion.LAST_ZOOM
import org.junit.Assert
import org.junit.Before
import org.junit.Test
@ -153,12 +153,6 @@ class UploadMediaDetailFragmentUnitTest {
Assert.assertNotNull(fragment)
}
@Test
@Throws(Exception::class)
fun testSetCallback() {
fragment.setCallback(null)
}
@Test
@Throws(Exception::class)
fun testOnCreate() {
@ -229,22 +223,6 @@ class UploadMediaDetailFragmentUnitTest {
method.invoke(fragment, R.string.media_detail_step_title, R.string.media_details_tooltip)
}
@Test
@Throws(Exception::class)
fun testOnNextButtonClicked() {
Shadows.shadowOf(Looper.getMainLooper()).idle()
Whitebox.setInternalState(fragment, "presenter", presenter)
fragment.onNextButtonClicked()
}
@Test
@Throws(Exception::class)
fun testOnPreviousButtonClicked() {
Shadows.shadowOf(Looper.getMainLooper()).idle()
Whitebox.setInternalState(fragment, "presenter", presenter)
fragment.onPreviousButtonClicked()
}
@Test
@Throws(Exception::class)
fun testShowSimilarImageFragment() {
@ -366,7 +344,10 @@ class UploadMediaDetailFragmentUnitTest {
`when`(uploadItem.gpsCoords).thenReturn(imageCoordinates)
val activityResult = ActivityResult(Activity.RESULT_OK, intent)
val handleResultMethod = UploadMediaDetailFragment::class.java.getDeclaredMethod("onCameraPosition", ActivityResult::class.java)
val handleResultMethod = UploadMediaDetailFragment::class.java.getDeclaredMethod(
"onCameraPosition",
ActivityResult::class.java
)
handleResultMethod.isAccessible = true
handleResultMethod.invoke(fragment, activityResult)
@ -382,7 +363,7 @@ class UploadMediaDetailFragmentUnitTest {
val cameraPosition = Mockito.mock(CameraPosition::class.java)
val latLng = Mockito.mock(LatLng::class.java)
Whitebox.setInternalState(fragment, "callback", callback)
Whitebox.setInternalState(fragment, "fragmentCallback", callback)
Whitebox.setInternalState(cameraPosition, "latitude", latLng.latitude)
Whitebox.setInternalState(cameraPosition, "longitude", latLng.longitude)
Whitebox.setInternalState(fragment, "editableUploadItem", uploadItem)
@ -394,9 +375,12 @@ class UploadMediaDetailFragmentUnitTest {
`when`(latLng.longitude).thenReturn(0.0)
`when`(uploadItem.gpsCoords).thenReturn(imageCoordinates)
val activityResult = ActivityResult(Activity.RESULT_OK,intent)
val activityResult = ActivityResult(Activity.RESULT_OK, intent)
val handleResultMethod = UploadMediaDetailFragment::class.java.getDeclaredMethod("onCameraPosition", ActivityResult::class.java)
val handleResultMethod = UploadMediaDetailFragment::class.java.getDeclaredMethod(
"onCameraPosition",
ActivityResult::class.java
)
handleResultMethod.isAccessible = true
handleResultMethod.invoke(fragment, activityResult)
@ -417,21 +401,6 @@ class UploadMediaDetailFragmentUnitTest {
fragment.onDestroyView()
}
@Test
@Throws(Exception::class)
fun testOnLlContainerTitleClicked() {
Shadows.shadowOf(Looper.getMainLooper()).idle()
fragment.onLlContainerTitleClicked()
}
@Test
@Throws(Exception::class)
fun testOnIbMapClicked() {
Shadows.shadowOf(Looper.getMainLooper()).idle()
Whitebox.setInternalState(fragment, "presenter", presenter)
fragment.onIbMapClicked()
}
@Test
@Throws(Exception::class)
fun testOnPrimaryCaptionTextChange() {