mirror of
https://github.com/commons-app/apps-android-commons.git
synced 2025-10-26 12:23:58 +01:00
[WIP]Lossless Transformations(Rotate feature) (#5252)
* UI setup for the crop feature almost setup * basic setup of rotate feature done * Added basic changes for editing feature * Getting data back from edit activity * Getting data back from edit activity * Updated contentUri * Finally the rotated image is getting uploaded * Minor Improvements for better testing * Fixed thumbnail preview * Fixed loss of exif data * Copy exif data * Save exif data * Added java docs * Minor fix * Added Javadoc * Refactoring * Formatting fixes * Minor Formatting Fix * Fix unit test * Add test coverage * Formatting fixes * Formatting Fixes --------- Co-authored-by: Priyank Shankar <priyankshankar@changejar.in>
This commit is contained in:
parent
6b8954b4a9
commit
2ddb6b2e5e
17 changed files with 570 additions and 28 deletions
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -48,9 +48,14 @@
|
|||
tools:ignore="GoogleAppIndexingWarning">
|
||||
|
||||
<activity
|
||||
android:theme="@style/EditActivityTheme"
|
||||
android:name=".description.DescriptionEditActivity"
|
||||
android:exported="true" />
|
||||
|
||||
<activity
|
||||
android:name=".edit.EditActivity"
|
||||
android:exported="false" />
|
||||
|
||||
<activity android:name="org.acra.dialog.CrashReportDialog"
|
||||
android:process=":acra"
|
||||
android:launchMode="singleInstance"
|
||||
|
|
|
|||
249
app/src/main/java/fr/free/nrw/commons/edit/EditActivity.kt
Normal file
249
app/src/main/java/fr/free/nrw/commons/edit/EditActivity.kt
Normal file
|
|
@ -0,0 +1,249 @@
|
|||
package fr.free.nrw.commons.edit
|
||||
|
||||
import android.animation.Animator
|
||||
import android.animation.Animator.AnimatorListener
|
||||
import android.animation.ValueAnimator
|
||||
import android.content.Intent
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.Matrix
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.media.ExifInterface
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.animation.AccelerateDecelerateInterpolator
|
||||
import android.widget.ImageView
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.graphics.rotationMatrix
|
||||
import androidx.core.graphics.scaleMatrix
|
||||
import androidx.core.net.toUri
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import fr.free.nrw.commons.R
|
||||
import kotlinx.android.synthetic.main.activity_edit.btn_save
|
||||
import kotlinx.android.synthetic.main.activity_edit.iv
|
||||
import kotlinx.android.synthetic.main.activity_edit.rotate_btn
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* An activity class for editing and rotating images using LLJTran with EXIF attribute preservation.
|
||||
*
|
||||
* This activity allows loads an image, allows users to rotate it by 90-degree increments, and
|
||||
* save the edited image while preserving its EXIF attributes. The class includes methods
|
||||
* for initializing the UI, animating image rotations, copying EXIF data, and handling
|
||||
* the image-saving process.
|
||||
*/
|
||||
class EditActivity : AppCompatActivity() {
|
||||
private var imageUri = ""
|
||||
private lateinit var vm: EditViewModel
|
||||
private val sourceExifAttributeList = mutableListOf<Pair<String, String?>>()
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
}
|
||||
27
app/src/main/java/fr/free/nrw/commons/edit/EditViewModel.kt
Normal file
27
app/src/main/java/fr/free/nrw/commons/edit/EditViewModel.kt
Normal file
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
21
app/src/main/java/fr/free/nrw/commons/edit/TransformImage.kt
Normal file
21
app/src/main/java/fr/free/nrw/commons/edit/TransformImage.kt
Normal file
|
|
@ -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?
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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<UploadMediaDetail> 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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,7 +625,8 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
|
|||
uploadMediaDetailAdapter.setItems(uploadMediaDetails);
|
||||
showNearbyFound =
|
||||
showNearbyFound && (
|
||||
uploadMediaDetails == null || uploadMediaDetails.isEmpty() || listContainsEmptyDetails(
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -37,6 +37,8 @@ public interface UploadMediaDetailsContract {
|
|||
|
||||
void showExternalMap(UploadItem uploadItem);
|
||||
|
||||
void showEditActivity(UploadItem uploadItem);
|
||||
|
||||
void updateMediaDetails(List<UploadMediaDetail> 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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<UploadMediaDetail> uploadMediaDetails = repository.getUploads()
|
||||
|
|
|
|||
5
app/src/main/res/drawable/baseline_rotate_right.xml
Normal file
5
app/src/main/res/drawable/baseline_rotate_right.xml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<vector android:height="24dp" android:tint="@color/item_white_background"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M15.55,5.55L11,1v3.07C7.06,4.56 4,7.92 4,12s3.05,7.44 7,7.93v-2.02c-2.84,-0.48 -5,-2.94 -5,-5.91s2.16,-5.43 5,-5.91L11,10l4.55,-4.45zM19.93,11c-0.17,-1.39 -0.72,-2.73 -1.62,-3.89l-1.42,1.42c0.54,0.75 0.88,1.6 1.02,2.47h2.02zM13,17.9v2.02c1.39,-0.17 2.74,-0.71 3.9,-1.61l-1.44,-1.44c-0.75,0.54 -1.59,0.89 -2.46,1.03zM16.89,15.48l1.42,1.41c0.9,-1.16 1.45,-2.5 1.62,-3.89h-2.02c-0.14,0.87 -0.48,1.72 -1.02,2.48z"/>
|
||||
</vector>
|
||||
5
app/src/main/res/drawable/baseline_save_24.xml
Normal file
5
app/src/main/res/drawable/baseline_save_24.xml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<vector android:height="24dp" android:tint="@color/bottom_bar_light"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M17,3L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,7l-4,-4zM12,19c-1.66,0 -3,-1.34 -3,-3s1.34,-3 3,-3 3,1.34 3,3 -1.34,3 -3,3zM15,9L5,9L5,5h10v4z"/>
|
||||
</vector>
|
||||
46
app/src/main/res/layout/activity_edit.xml
Normal file
46
app/src/main/res/layout/activity_edit.xml
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="bottom"
|
||||
tools:context=".edit.EditActivity"
|
||||
android:gravity="center"
|
||||
android:layout_margin="2dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/iv"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center"
|
||||
/>
|
||||
<LinearLayout
|
||||
android:elevation="2dp"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal"
|
||||
android:layout_gravity="bottom|center"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="2dp">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatButton
|
||||
android:drawablePadding="4dp"
|
||||
android:drawableStart="@drawable/baseline_rotate_right"
|
||||
android:id="@+id/rotate_btn"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@android:color/white"
|
||||
android:text="@string/rotate"/>
|
||||
|
||||
<androidx.appcompat.widget.AppCompatButton
|
||||
android:onClick="getRotatedImage"
|
||||
android:drawablePadding="4dp"
|
||||
android:drawableStart="@drawable/baseline_save_24"
|
||||
android:id="@+id/btn_save"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/menu_save_categories"
|
||||
android:textColor="@android:color/white" />
|
||||
</LinearLayout>
|
||||
</FrameLayout>
|
||||
|
|
@ -142,6 +142,16 @@
|
|||
android:layout_marginRight="@dimen/standard_gap"
|
||||
android:layout_toLeftOf="@+id/btn_next"
|
||||
android:text="@string/previous" />
|
||||
<Button
|
||||
android:id="@+id/edit_image"
|
||||
style="@style/Widget.AppCompat.Button.Borderless"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="@dimen/standard_gap"
|
||||
android:layout_marginBottom="24dp"
|
||||
android:layout_toStartOf="@id/btn_previous"
|
||||
android:contentDescription="Edit Image"
|
||||
android:text="Edit Image" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
|
|
|
|||
|
|
@ -353,6 +353,7 @@
|
|||
<string name="wrong">Wrong Answer</string>
|
||||
<string name="quiz_screenshot_question">Is this screenshot OK to upload?</string>
|
||||
<string name="share_app_title">Share App</string>
|
||||
<string name="rotate">Rotate</string>
|
||||
|
||||
<string name="error_fetching_nearby_places">Error fetching nearby places.</string>
|
||||
<string name="no_nearby_places_around">No nearby places around</string>
|
||||
|
|
|
|||
|
|
@ -158,6 +158,9 @@
|
|||
<style name="LightFlatNearbyPermissionButton" parent="LightAppTheme">
|
||||
<item name="colorControlHighlight">@color/primaryColor</item>
|
||||
</style>
|
||||
<style name="EditActivityTheme" parent="Theme.AppCompat.DayNight.DarkActionBar">
|
||||
<item name="colorPrimary">@color/primaryColor</item>
|
||||
</style>
|
||||
|
||||
<style name="ProgressBar" parent="Widget.AppCompat.ProgressBar.Horizontal" />
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue