mirror of
				https://github.com/commons-app/apps-android-commons.git
				synced 2025-10-31 06:43:56 +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
				
			
		
							
								
								
									
										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,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); | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
|  | @ -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() | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Priyank Shankar
						Priyank Shankar