diff --git a/app/build.gradle b/app/build.gradle
index 5d782320f..4d0fece84 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -140,6 +140,8 @@ dependencies {
implementation "androidx.preference:preference:$PREFERENCE_VERSION"
// Kotlin
implementation "androidx.preference:preference-ktx:$PREFERENCE_VERSION"
+ //Android Media
+ implementation 'com.github.juanitobananas:AndroidMediaUtil:v1.0-1'
implementation "androidx.multidex:multidex:$MULTIDEX_VERSION"
@@ -236,8 +238,8 @@ android {
}
}
debug {
- minifyEnabled false
testCoverageEnabled true
+ minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
testProguardFile 'test-proguard-rules.txt'
versionNameSuffix "-debug-" + getBranchName()
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index cee5fb81a..5ba49201a 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -48,9 +48,14 @@
tools:ignore="GoogleAppIndexingWarning">
+
+
>()
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_edit)
+ supportActionBar?.title = ""
+ val intent = intent
+ imageUri = intent.getStringExtra("image") ?: ""
+ vm = ViewModelProvider(this).get(EditViewModel::class.java)
+ val sourceExif = imageUri.toUri().path?.let { ExifInterface(it) }
+ val exifTags = arrayOf(
+ ExifInterface.TAG_APERTURE,
+ ExifInterface.TAG_DATETIME,
+ ExifInterface.TAG_EXPOSURE_TIME,
+ ExifInterface.TAG_FLASH,
+ ExifInterface.TAG_FOCAL_LENGTH,
+ ExifInterface.TAG_GPS_ALTITUDE,
+ ExifInterface.TAG_GPS_ALTITUDE_REF,
+ ExifInterface.TAG_GPS_DATESTAMP,
+ ExifInterface.TAG_GPS_LATITUDE,
+ ExifInterface.TAG_GPS_LATITUDE_REF,
+ ExifInterface.TAG_GPS_LONGITUDE,
+ ExifInterface.TAG_GPS_LONGITUDE_REF,
+ ExifInterface.TAG_GPS_PROCESSING_METHOD,
+ ExifInterface.TAG_GPS_TIMESTAMP,
+ ExifInterface.TAG_IMAGE_LENGTH,
+ ExifInterface.TAG_IMAGE_WIDTH,
+ ExifInterface.TAG_ISO,
+ ExifInterface.TAG_MAKE,
+ ExifInterface.TAG_MODEL,
+ ExifInterface.TAG_ORIENTATION,
+ ExifInterface.TAG_WHITE_BALANCE,
+ ExifInterface.WHITEBALANCE_AUTO,
+ ExifInterface.WHITEBALANCE_MANUAL
+ )
+ for (tag in exifTags) {
+ val attribute = sourceExif?.getAttribute(tag.toString())
+ sourceExifAttributeList.add(Pair(tag.toString(), attribute))
+ }
+
+ init()
+ }
+
+ /**
+ * Initializes the ImageView and associated UI elements.
+ *
+ * This function sets up the ImageView for displaying an image, adjusts its view bounds,
+ * and scales the initial image to fit within the ImageView. It also sets click listeners
+ * for the "Rotate" and "Save" buttons.
+ */
+ private fun init() {
+ iv.adjustViewBounds = true
+ iv.scaleType = ImageView.ScaleType.MATRIX
+ iv.post(Runnable {
+ val bitmap = BitmapFactory.decodeFile(imageUri)
+ iv.setImageBitmap(bitmap)
+ if (bitmap.width > 0) {
+ val scale =
+ iv.measuredWidth.toFloat() / (iv.drawable as BitmapDrawable).bitmap.width.toFloat()
+ iv.layoutParams.height =
+ (scale * (iv.drawable as BitmapDrawable).bitmap.height).toInt()
+ iv.imageMatrix = scaleMatrix(scale, scale)
+ }
+ })
+ rotate_btn.setOnClickListener {
+ animateImageHeight()
+ }
+ btn_save.setOnClickListener {
+ getRotatedImage()
+ }
+ }
+
+ var imageRotation = 0
+
+ /**
+ * Animates the height, rotation, and scale of an ImageView to provide a smooth
+ * transition effect when rotating an image by 90 degrees.
+ *
+ * This function calculates the new height, rotation, and scale for the ImageView
+ * based on the current image rotation angle and animates the changes using a
+ * ValueAnimator. It also disables a rotate button during the animation to prevent
+ * further rotation actions.
+ */
+ private fun animateImageHeight() {
+ val drawableWidth: Float = iv.getDrawable().getIntrinsicWidth().toFloat()
+ val drawableHeight: Float = iv.getDrawable().getIntrinsicHeight().toFloat()
+ val viewWidth: Float = iv.getMeasuredWidth().toFloat()
+ val viewHeight: Float = iv.getMeasuredHeight().toFloat()
+ val rotation = imageRotation % 360
+ val newRotation = rotation + 90
+
+ val newViewHeight: Int
+ val imageScale: Float
+ val newImageScale: Float
+
+ Timber.d("Rotation $rotation")
+ Timber.d("new Rotation $newRotation")
+
+
+ if (rotation == 0 || rotation == 180) {
+ imageScale = viewWidth / drawableWidth
+ newImageScale = viewWidth / drawableHeight
+ newViewHeight = (drawableWidth * newImageScale).toInt()
+ } else if (rotation == 90 || rotation == 270) {
+ imageScale = viewWidth / drawableHeight
+ newImageScale = viewWidth / drawableWidth
+ newViewHeight = (drawableHeight * newImageScale).toInt()
+ } else {
+ throw UnsupportedOperationException("rotation can 0, 90, 180 or 270. \${rotation} is unsupported")
+ }
+
+ val animator = ValueAnimator.ofFloat(0f, 1f).setDuration(1000L)
+
+ animator.interpolator = AccelerateDecelerateInterpolator()
+
+ animator.addListener(object : AnimatorListener {
+ override fun onAnimationStart(animation: Animator) {
+ rotate_btn.setEnabled(false)
+ }
+
+ override fun onAnimationEnd(animation: Animator) {
+ imageRotation = newRotation % 360
+ rotate_btn.setEnabled(true)
+ }
+
+ override fun onAnimationCancel(animation: Animator) {
+ }
+
+ override fun onAnimationRepeat(animation: Animator) {
+ }
+
+ })
+
+ animator.addUpdateListener { animation ->
+ val animVal = animation.animatedValue as Float
+ val complementaryAnimVal = 1 - animVal
+ val animatedHeight =
+ (complementaryAnimVal * viewHeight + animVal * newViewHeight).toInt()
+ val animatedScale = complementaryAnimVal * imageScale + animVal * newImageScale
+ val animatedRotation = complementaryAnimVal * rotation + animVal * newRotation
+ iv.getLayoutParams().height = animatedHeight
+ val matrix: Matrix = rotationMatrix(
+ animatedRotation,
+ drawableWidth / 2,
+ drawableHeight / 2
+ )
+ matrix.postScale(
+ animatedScale,
+ animatedScale,
+ drawableWidth / 2,
+ drawableHeight / 2
+ )
+ matrix.postTranslate(
+ -(drawableWidth - iv.getMeasuredWidth()) / 2,
+ -(drawableHeight - iv.getMeasuredHeight()) / 2
+ )
+ iv.setImageMatrix(matrix)
+ iv.requestLayout()
+ }
+
+ animator.start()
+ }
+
+ /**
+ * Rotates and edits the current image, copies EXIF data, and returns the edited image path.
+ *
+ * This function retrieves the path of the current image specified by `imageUri`,
+ * rotates it based on the `imageRotation` angle using the `rotateImage` method
+ * from the `vm`, and updates the EXIF attributes of the
+ * rotated image based on the `sourceExifAttributeList`. It then copies the EXIF data
+ * using the `copyExifData` method, creates an Intent to return the edited image's file path
+ * as a result, and finishes the current activity.
+ */
+ fun getRotatedImage() {
+
+ val filePath = imageUri.toUri().path
+ val file = filePath?.let { File(it) }
+
+
+ val rotatedImage = file?.let { vm.rotateImage(imageRotation, it) }
+ if (rotatedImage == null) {
+ Toast.makeText(this, "Failed to rotate to image", Toast.LENGTH_LONG).show()
+ }
+ val editedImageExif = rotatedImage?.path?.let { ExifInterface(it) }
+ copyExifData(editedImageExif)
+ val resultIntent = Intent()
+ resultIntent.putExtra("editedImageFilePath", rotatedImage?.toUri()?.path ?: "Error");
+ setResult(RESULT_OK, resultIntent);
+ finish();
+ }
+
+ /**
+ * Copies EXIF data from sourceExifAttributeList to the provided ExifInterface object.
+ *
+ * This function iterates over the `sourceExifAttributeList` and sets the EXIF attributes
+ * on the provided `editedImageExif` object.
+ *
+ * @param editedImageExif The ExifInterface object for the edited image.
+ */
+ private fun copyExifData(editedImageExif: ExifInterface?) {
+
+ for (attr in sourceExifAttributeList) {
+ Log.d("Tag is ${attr.first}", "Value is ${attr.second}")
+ editedImageExif!!.setAttribute(attr.first, attr.second)
+ Log.d("Tag is ${attr.first}", "Value is ${attr.second}")
+ }
+
+ editedImageExif?.saveAttributes()
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/fr/free/nrw/commons/edit/EditViewModel.kt b/app/src/main/java/fr/free/nrw/commons/edit/EditViewModel.kt
new file mode 100644
index 000000000..80db0f1ab
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/edit/EditViewModel.kt
@@ -0,0 +1,27 @@
+package fr.free.nrw.commons.edit
+
+import androidx.lifecycle.ViewModel
+import java.io.File
+
+/**
+ * ViewModel for image editing operations.
+ *
+ * This ViewModel class is responsible for managing image editing operations, such as
+ * rotating images. It utilizes a TransformImage implementation to perform image transformations.
+ */
+class EditViewModel() : ViewModel() {
+
+ // Ideally should be injected using DI
+ private val transformImage: TransformImage = TransformImageImpl()
+
+ /**
+ * Rotates the specified image file by the given degree.
+ *
+ * @param degree The degree by which to rotate the image.
+ * @param imageFile The File representing the image to be rotated.
+ * @return The rotated image File, or null if the rotation operation fails.
+ */
+ fun rotateImage(degree: Int, imageFile: File): File? {
+ return transformImage.rotateImage(imageFile, degree)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/fr/free/nrw/commons/edit/TransformImage.kt b/app/src/main/java/fr/free/nrw/commons/edit/TransformImage.kt
new file mode 100644
index 000000000..4c3607a8e
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/edit/TransformImage.kt
@@ -0,0 +1,21 @@
+package fr.free.nrw.commons.edit
+
+import java.io.File
+
+/**
+ * Interface for image transformation operations.
+ *
+ * This interface defines a contract for image transformation operations, allowing
+ * implementations to provide specific functionality for tasks like rotating images.
+ */
+interface TransformImage {
+
+ /**
+ * Rotates the specified image file by the given degree.
+ *
+ * @param imageFile The File representing the image to be rotated.
+ * @param degree The degree by which to rotate the image.
+ * @return The rotated image File, or null if the rotation operation fails.
+ */
+ fun rotateImage(imageFile: File, degree : Int ):File?
+}
\ No newline at end of file
diff --git a/app/src/main/java/fr/free/nrw/commons/edit/TransformImageImpl.kt b/app/src/main/java/fr/free/nrw/commons/edit/TransformImageImpl.kt
new file mode 100644
index 000000000..fb96ca044
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/edit/TransformImageImpl.kt
@@ -0,0 +1,74 @@
+package fr.free.nrw.commons.edit
+
+import android.mediautil.image.jpeg.LLJTran
+import android.mediautil.image.jpeg.LLJTranException
+import android.os.Environment
+import timber.log.Timber
+import java.io.BufferedOutputStream
+import java.io.File
+import java.io.FileOutputStream
+
+/**
+ * Implementation of the TransformImage interface for image rotation operations.
+ *
+ * This class provides an implementation for the TransformImage interface, right now it exposes a
+ * function for rotating images by a specified degree using the LLJTran library. Right now it reads
+ * the input image file, performs the rotation, and saves the rotated image to a new file.
+ */
+class TransformImageImpl() : TransformImage {
+
+ /**
+ * Rotates the specified image file by the given degree.
+ *
+ * @param imageFile The File representing the image to be rotated.
+ * @param degree The degree by which to rotate the image.
+ * @return The rotated image File, or null if the rotation operation fails.
+ */
+ override fun rotateImage(imageFile: File, degree : Int): File? {
+
+ Timber.tag("Trying to rotate image").d("Starting")
+
+ val path = Environment.getExternalStoragePublicDirectory(
+ Environment.DIRECTORY_DOWNLOADS
+ )
+
+ val imagePath = System.currentTimeMillis()
+ val file: File = File(path, "$imagePath.jpg")
+
+ val output = file
+
+ val rotated = try {
+ val lljTran = LLJTran(imageFile)
+ lljTran.read(
+ LLJTran.READ_ALL,
+ false,
+ ) // This could throw an LLJTranException. I am not catching it for now... Let's see.
+ lljTran.transform(
+ when(degree){
+ 90 -> LLJTran.ROT_90
+ 180 -> LLJTran.ROT_180
+ 270 -> LLJTran.ROT_270
+ else -> {
+ LLJTran.ROT_90
+ }
+ },
+ LLJTran.OPT_DEFAULTS or LLJTran.OPT_XFORM_ORIENTATION
+ )
+ BufferedOutputStream(FileOutputStream(output)).use { writer ->
+ lljTran.save(writer, LLJTran.OPT_WRITE_ALL )
+ }
+ lljTran.freeMemory()
+ true
+ } catch (e: LLJTranException) {
+ Timber.tag("Error").d(e)
+ return null
+ false
+ }
+
+ if (rotated) {
+ Timber.tag("Done rotating image").d("Done")
+ Timber.tag("Add").d(output.absolutePath)
+ }
+ return output
+ }
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.java
index 8351ec7c4..415629256 100644
--- a/app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.java
+++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.java
@@ -65,6 +65,8 @@ import fr.free.nrw.commons.utils.ViewUtil;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.schedulers.Schedulers;
+import java.io.File;
+import java.security.Permission;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
@@ -484,6 +486,23 @@ public class UploadActivity extends BaseActivity implements UploadContract.View,
presenter.deletePictureAtIndex(index);
}
+ /**
+ * Changes the thumbnail of an UploadableFile at the specified index.
+ * This method updates the list of uploadableFiles by replacing the UploadableFile
+ * at the given index with a new UploadableFile created from the provided file path.
+ * After updating the list, it notifies the RecyclerView's adapter to refresh its data,
+ * 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.
+ */
+ @Override
+ public void changeThumbnail(int index, String filepath) {
+ uploadableFiles.remove(index);
+ uploadableFiles.add(index, new UploadableFile(new File(filepath)));
+ rvThumbnails.getAdapter().notifyDataSetChanged();
+ }
+
@Override
public void onNextButtonClicked(int index) {
UploadActivity.this.onNextButtonClicked(index);
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadItem.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadItem.java
index 87050fb5c..b3c16b962 100644
--- a/app/src/main/java/fr/free/nrw/commons/upload/UploadItem.java
+++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadItem.java
@@ -14,7 +14,7 @@ import java.util.List;
public class UploadItem {
- private final Uri mediaUri;
+ private Uri mediaUri;
private final String mimeType;
private ImageCoordinates gpsCoords;
private List uploadMediaDetails;
@@ -31,7 +31,7 @@ public class UploadItem {
* Uri of uploadItem
* Uri points to image location or name, eg content://media/external/images/camera/10495 (Android 10)
*/
- private final Uri contentUri;
+ private Uri contentUri;
@SuppressLint("CheckResult")
@@ -160,4 +160,16 @@ public class UploadItem {
public String getCountryCode() {
return countryCode;
}
+
+ /**
+ * Sets both the contentUri and mediaUri to the specified Uri.
+ * This method allows you to assign the same Uri to both the contentUri and mediaUri
+ * properties.
+ *
+ * @param uri The Uri to be set as both the contentUri and mediaUri.
+ */
+ public void setContentUri(Uri uri) {
+ contentUri = uri;
+ mediaUri = uri;
+ }
}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragment.java b/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragment.java
index 9cbf78e8c..a2da97696 100644
--- a/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragment.java
+++ b/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragment.java
@@ -6,7 +6,10 @@ import static fr.free.nrw.commons.utils.ImageUtils.FILE_NAME_EXISTS;
import static fr.free.nrw.commons.utils.ImageUtils.getErrorMessageForResult;
import android.annotation.SuppressLint;
+import android.content.ContentResolver;
+import android.content.Context;
import android.content.Intent;
+import android.net.Uri;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.LayoutInflater;
@@ -32,6 +35,7 @@ import com.github.chrisbanes.photoview.PhotoView;
import com.mapbox.mapboxsdk.camera.CameraPosition;
import fr.free.nrw.commons.LocationPicker.LocationPicker;
import fr.free.nrw.commons.R;
+import fr.free.nrw.commons.edit.EditActivity;
import fr.free.nrw.commons.contributions.MainActivity;
import fr.free.nrw.commons.filepicker.UploadableFile;
import fr.free.nrw.commons.kvstore.JsonKvStore;
@@ -51,6 +55,7 @@ import fr.free.nrw.commons.utils.DialogUtil;
import fr.free.nrw.commons.utils.ImageUtils;
import fr.free.nrw.commons.utils.ViewUtil;
import fr.free.nrw.commons.R.drawable.*;
+import java.io.File;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
@@ -63,10 +68,11 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
UploadMediaDetailsContract.View, UploadMediaDetailAdapter.EventListener {
private static final int REQUEST_CODE = 1211;
+ private static final int REQUEST_CODE_FOR_EDIT_ACTIVITY = 1212;
+
/**
- * A key for applicationKvStore.
- * By this key we can retrieve the location of last UploadItem ex. 12.3433,54.78897
- * from applicationKvStore.
+ * 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";
@@ -86,8 +92,11 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
AppCompatButton btnNext;
@BindView(R.id.btn_previous)
AppCompatButton btnPrevious;
+ @BindView(R.id.edit_image)
+ AppCompatButton editImage;
@BindView(R.id.tooltip)
ImageView tooltip;
+
private UploadMediaDetailAdapter uploadMediaDetailAdapter;
@BindView(R.id.btn_copy_subsequent_media)
AppCompatButton btnCopyToSubsequentMedia;
@@ -108,14 +117,14 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
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).
+ * 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
+ * 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;
@@ -125,8 +134,8 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
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
+ * inAppPictureLocation: use location recorded while using the in-app camera if device camera
+ * does not record it in the EXIF
*/
private LatLng inAppPictureLocation;
/**
@@ -145,7 +154,8 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
super.onCreate(savedInstanceState);
}
- public void setImageTobeUploaded(UploadableFile uploadableFile, Place place, LatLng inAppPictureLocation) {
+ public void setImageTobeUploaded(UploadableFile uploadableFile, Place place,
+ LatLng inAppPictureLocation) {
this.uploadableFile = uploadableFile;
this.place = place;
this.inAppPictureLocation = inAppPictureLocation;
@@ -197,7 +207,7 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
}
//If this is the last media, we have nothing to copy, lets not show the button
- if (callback.getIndexInViewFlipper(this) == callback.getTotalNumberOfSteps()-4) {
+ if (callback.getIndexInViewFlipper(this) == callback.getTotalNumberOfSteps() - 4) {
btnCopyToSubsequentMedia.setVisibility(View.GONE);
} else {
btnCopyToSubsequentMedia.setVisibility(View.VISIBLE);
@@ -228,7 +238,8 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
* init the description recycler veiw and caption recyclerview
*/
private void initRecyclerView() {
- uploadMediaDetailAdapter = new UploadMediaDetailAdapter(defaultKvStore.getString(Prefs.DESCRIPTION_LANGUAGE, ""), recentLanguagesDao);
+ uploadMediaDetailAdapter = new UploadMediaDetailAdapter(
+ defaultKvStore.getString(Prefs.DESCRIPTION_LANGUAGE, ""), recentLanguagesDao);
uploadMediaDetailAdapter.setCallback(this::showInfoAlert);
uploadMediaDetailAdapter.setEventListener(this);
rvDescriptions.setLayoutManager(new LinearLayoutManager(getContext()));
@@ -241,7 +252,8 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
* @param messageStringId
*/
private void showInfoAlert(int titleStringID, int messageStringId) {
- DialogUtil.showAlertDialog(getActivity(), getString(titleStringID), getString(messageStringId), getString(android.R.string.ok), null, true);
+ DialogUtil.showAlertDialog(getActivity(), getString(titleStringID),
+ getString(messageStringId), getString(android.R.string.ok), null, true);
}
@OnClick(R.id.btn_next)
@@ -267,6 +279,10 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
rvDescriptions.smoothScrollToPosition(uploadMediaDetailAdapter.getItemCount()-1);
}
+ @OnClick(R.id.edit_image)
+ public void onEditButtonClicked() {
+ presenter.onEditButtonClicked(callback.getIndexInViewFlipper(this));
+ }
@Override
public void showSimilarImageFragment(String originalFilePath, String possibleFilePath,
ImageCoordinates similarImageCoordinates) {
@@ -312,7 +328,7 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
nearbyPlace = place;
this.uploadItem = uploadItem;
showNearbyFound = true;
- if(callback.getIndexInViewFlipper(this) == 0) {
+ if (callback.getIndexInViewFlipper(this) == 0) {
if (UploadActivity.nearbyPopupAnswers.containsKey(nearbyPlace)) {
final boolean response = UploadActivity.nearbyPopupAnswers.get(nearbyPlace);
if (response) {
@@ -330,7 +346,8 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
* 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
+ @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);
@@ -367,7 +384,7 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
protected void onBecameVisible() {
super.onBecameVisible();
presenter.fetchTitleAndDescription(callback.getIndexInViewFlipper(this));
- if(showNearbyFound) {
+ if (showNearbyFound) {
if (UploadActivity.nearbyPopupAnswers.containsKey(nearbyPlace)) {
final boolean response = UploadActivity.nearbyPopupAnswers.get(nearbyPlace);
if (response) {
@@ -465,6 +482,24 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
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 with a request code, allowing the result to be handled in onActivityResult.
+ */
+ @Override
+ public void showEditActivity(UploadItem uploadItem) {
+ editableUploadItem = uploadItem;
+ Intent intent = new Intent(getContext(), EditActivity.class);
+ intent.putExtra("image", uploadableFile.getFilePath().toString());
+ startActivityForResult(intent, REQUEST_CODE_FOR_EDIT_ACTIVITY);
+ }
+
/**
* Start Location picker activity. Show the location first then user can modify it by clicking
* modify location button.
@@ -500,7 +535,7 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
defaultLatitude = Double.parseDouble(locationLatLng[0]);
defaultLongitude = Double.parseDouble(locationLatLng[1]);
}
- if(defaultKvStore.getString(LAST_ZOOM) != null){
+ if (defaultKvStore.getString(LAST_ZOOM) != null) {
defaultZoom = Double.parseDouble(defaultKvStore.getString(LAST_ZOOM));
}
startActivityForResult(new LocationPicker.IntentBuilder()
@@ -535,17 +570,33 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
final String longitude = String.valueOf(cameraPosition.target.getLongitude());
final double zoom = cameraPosition.zoom;
- editLocation(latitude, longitude,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){
+ if (isMissingLocationDialog) {
isMissingLocationDialog = false;
onNextButtonClicked();
}
}
}
+ if (requestCode == REQUEST_CODE_FOR_EDIT_ACTIVITY && resultCode == RESULT_OK) {
+ String result = data.getStringExtra("editedImageFilePath");
+
+ if (Objects.equals(result, "Error")) {
+ Timber.e("Error in rotating image");
+ return;
+ }
+ try {
+ photoViewBackgroundImage.setImageURI(Uri.fromFile(new File(result)));
+ editableUploadItem.setContentUri(Uri.fromFile(new File(result)));
+ callback.changeThumbnail(callback.getIndexInViewFlipper(this),
+ result);
+ } catch (Exception e) {
+ Timber.e(e);
+ }
+ }
}
/**
@@ -553,11 +604,11 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
* @param latitude new latitude
* @param longitude new longitude
*/
- public void editLocation(final String latitude, final String longitude, final double zoom){
+ 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().setDecimalCoords(latitude + "|" + longitude);
editableUploadItem.getGpsCoords().setImageCoordsExists(true);
editableUploadItem.getGpsCoords().setZoomLevel(zoom);
@@ -574,8 +625,9 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
uploadMediaDetailAdapter.setItems(uploadMediaDetails);
showNearbyFound =
showNearbyFound && (
- uploadMediaDetails == null || uploadMediaDetails.isEmpty() || listContainsEmptyDetails(
- uploadMediaDetails));
+ uploadMediaDetails == null || uploadMediaDetails.isEmpty()
+ || listContainsEmptyDetails(
+ uploadMediaDetails));
}
/**
@@ -647,15 +699,17 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
public void onPrimaryCaptionTextChange(boolean isNotEmpty) {
btnCopyToSubsequentMedia.setEnabled(isNotEmpty);
btnCopyToSubsequentMedia.setClickable(isNotEmpty);
- btnCopyToSubsequentMedia.setAlpha(isNotEmpty ? 1.0f: 0.5f);
+ btnCopyToSubsequentMedia.setAlpha(isNotEmpty ? 1.0f : 0.5f);
btnNext.setEnabled(isNotEmpty);
btnNext.setClickable(isNotEmpty);
- btnNext.setAlpha(isNotEmpty ? 1.0f: 0.5f);
+ btnNext.setAlpha(isNotEmpty ? 1.0f : 0.5f);
}
public interface UploadMediaDetailFragmentCallback extends Callback {
void deletePictureAtIndex(int index);
+
+ void changeThumbnail(int index, String uri);
}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailsContract.java b/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailsContract.java
index b87c4fc99..fbd642668 100644
--- a/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailsContract.java
+++ b/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailsContract.java
@@ -37,6 +37,8 @@ public interface UploadMediaDetailsContract {
void showExternalMap(UploadItem uploadItem);
+ void showEditActivity(UploadItem uploadItem);
+
void updateMediaDetails(List uploadMediaDetails);
void displayAddLocationDialog(Runnable runnable);
@@ -56,6 +58,8 @@ public interface UploadMediaDetailsContract {
void onMapIconClicked(int indexInViewFlipper);
+ void onEditButtonClicked(int indexInViewFlipper);
+
void onUserConfirmedUploadIsOfPlace(Place place, int uploadItemPosition);
}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaPresenter.java b/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaPresenter.java
index 866f87584..3a822df7c 100644
--- a/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaPresenter.java
+++ b/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaPresenter.java
@@ -281,6 +281,11 @@ public class UploadMediaPresenter implements UserActionListener, SimilarImageInt
view.showExternalMap(repository.getUploads().get(indexInViewFlipper));
}
+ @Override
+ public void onEditButtonClicked(int indexInViewFlipper){
+ view.showEditActivity(repository.getUploads().get(indexInViewFlipper));
+ }
+
@Override
public void onUserConfirmedUploadIsOfPlace(Place place, int uploadItemPosition) {
final List uploadMediaDetails = repository.getUploads()
diff --git a/app/src/main/res/drawable/baseline_rotate_right.xml b/app/src/main/res/drawable/baseline_rotate_right.xml
new file mode 100644
index 000000000..8a90343f4
--- /dev/null
+++ b/app/src/main/res/drawable/baseline_rotate_right.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/drawable/baseline_save_24.xml b/app/src/main/res/drawable/baseline_save_24.xml
new file mode 100644
index 000000000..923fe8ff6
--- /dev/null
+++ b/app/src/main/res/drawable/baseline_save_24.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/layout/activity_edit.xml b/app/src/main/res/layout/activity_edit.xml
new file mode 100644
index 000000000..e64416bfe
--- /dev/null
+++ b/app/src/main/res/layout/activity_edit.xml
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_upload_media_detail_fragment.xml b/app/src/main/res/layout/fragment_upload_media_detail_fragment.xml
index 52b143f82..9e493e768 100644
--- a/app/src/main/res/layout/fragment_upload_media_detail_fragment.xml
+++ b/app/src/main/res/layout/fragment_upload_media_detail_fragment.xml
@@ -142,6 +142,16 @@
android:layout_marginRight="@dimen/standard_gap"
android:layout_toLeftOf="@+id/btn_next"
android:text="@string/previous" />
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 3cff4fc3d..a926e8108 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -353,6 +353,7 @@
Wrong AnswerIs this screenshot OK to upload?Share App
+ RotateError fetching nearby places.No nearby places around
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index 22b6dc720..2de248129 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -158,6 +158,9 @@
+