[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:
Priyank Shankar 2023-10-24 09:34:21 +05:30 committed by GitHub
parent 6b8954b4a9
commit 2ddb6b2e5e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 570 additions and 28 deletions

View file

@ -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()

View file

@ -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"

View 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()
}
}

View 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)
}
}

View 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?
}

View 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
}
}

View file

@ -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);

View file

@ -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;
}
}

View file

@ -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;
@ -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) {
@ -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);
@ -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.
@ -546,6 +581,22 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
}
}
}
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);
}
}
}
/**
@ -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));
}
@ -656,6 +708,8 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
public interface UploadMediaDetailFragmentCallback extends Callback {
void deletePictureAtIndex(int index);
void changeThumbnail(int index, String uri);
}

View file

@ -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);
}

View file

@ -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()

View 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>

View 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>

View 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>

View file

@ -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>

View file

@ -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>

View file

@ -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" />