Replace image cropping library

This commit is contained in:
ThomasSession 2024-08-09 09:35:03 +10:00 committed by fanchao
parent 62873ee773
commit 26b186452a
4 changed files with 197 additions and 145 deletions

View File

@ -287,7 +287,7 @@ dependencies {
implementation 'com.pnikosis:materialish-progress:1.5'
implementation 'org.greenrobot:eventbus:3.0.0'
implementation 'pl.tajchert:waitingdots:0.1.0'
implementation 'com.theartofdev.edmodo:android-image-cropper:2.8.0'
implementation 'com.vanniktech:android-image-cropper:4.5.0'
implementation 'com.melnykov:floatingactionbutton:1.3.0'
implementation 'com.google.zxing:android-integration:3.1.0'
implementation "com.google.dagger:hilt-android:$daggerVersion"

View File

@ -1,116 +0,0 @@
package org.thoughtcrime.securesms.avatar;
import android.Manifest;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.provider.MediaStore;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.core.content.ContextCompat;
import com.theartofdev.edmodo.cropper.CropImage;
import com.theartofdev.edmodo.cropper.CropImageView;
import org.session.libsignal.utilities.NoExternalStorageException;
import org.session.libsignal.utilities.Log;
import org.session.libsignal.utilities.ExternalStorageUtil;
import org.thoughtcrime.securesms.util.FileProviderUtil;
import org.thoughtcrime.securesms.util.IntentUtils;
import java.io.File;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import network.loki.messenger.R;
import static android.provider.MediaStore.EXTRA_OUTPUT;
public final class AvatarSelection {
private static final String TAG = AvatarSelection.class.getSimpleName();
public static final int REQUEST_CODE_CROP_IMAGE = CropImage.CROP_IMAGE_ACTIVITY_REQUEST_CODE;
public static final int REQUEST_CODE_AVATAR = REQUEST_CODE_CROP_IMAGE + 1;
private AvatarSelection() {
}
/**
* Returns result on {@link #REQUEST_CODE_CROP_IMAGE}
*/
public static void circularCropImage(Activity activity, Uri inputFile, Uri outputFile, @StringRes int title) {
CropImage.activity(inputFile)
.setGuidelines(CropImageView.Guidelines.ON)
.setAspectRatio(1, 1)
.setCropShape(Build.VERSION.SDK_INT >= Build.VERSION_CODES.P ? CropImageView.CropShape.RECTANGLE : CropImageView.CropShape.OVAL)
.setOutputUri(outputFile)
.setAllowRotation(true)
.setAllowFlipping(true)
.setBackgroundColor(ContextCompat.getColor(activity, R.color.avatar_background))
.setActivityTitle(activity.getString(title))
.start(activity);
}
public static Uri getResultUri(Intent data) {
return CropImage.getActivityResult(data).getUri();
}
/**
* Returns result on {@link #REQUEST_CODE_AVATAR}
*
* @return Temporary capture file if created.
*/
public static File startAvatarSelection(Activity activity, boolean includeClear, boolean attemptToIncludeCamera) {
File captureFile = null;
boolean hasCameraPermission = ContextCompat
.checkSelfPermission(activity, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED;
if (attemptToIncludeCamera && hasCameraPermission) {
try {
captureFile = File.createTempFile("avatar-capture", ".jpg", ExternalStorageUtil.getImageDir(activity));
} catch (IOException | NoExternalStorageException e) {
Log.e("Cannot reserve a temporary avatar capture file.", e);
}
}
Intent chooserIntent = createAvatarSelectionIntent(activity, captureFile, includeClear);
activity.startActivityForResult(chooserIntent, REQUEST_CODE_AVATAR);
return captureFile;
}
private static Intent createAvatarSelectionIntent(Context context, @Nullable File tempCaptureFile, boolean includeClear) {
List<Intent> extraIntents = new LinkedList<>();
Intent galleryIntent = new Intent(Intent.ACTION_PICK);
galleryIntent.setDataAndType(android.provider.MediaStore.Images.Media.INTERNAL_CONTENT_URI, "image/*");
if (!IntentUtils.isResolvable(context, galleryIntent)) {
galleryIntent = new Intent(Intent.ACTION_GET_CONTENT);
galleryIntent.setType("image/*");
}
if (tempCaptureFile != null) {
Uri uri = FileProviderUtil.getUriFor(context, tempCaptureFile);
Intent cameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
cameraIntent.putExtra(EXTRA_OUTPUT, uri);
cameraIntent.setFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
extraIntents.add(cameraIntent);
}
if (includeClear) {
extraIntents.add(new Intent("network.loki.securesms.action.CLEAR_PROFILE_PHOTO"));
}
Intent chooserIntent = Intent.createChooser(galleryIntent, context.getString(R.string.CreateProfileActivity_profile_photo));
if (!extraIntents.isEmpty()) {
chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, extraIntents.toArray(new Intent[0]));
}
return chooserIntent;
}
}

View File

@ -0,0 +1,141 @@
package org.thoughtcrime.securesms.avatar
import android.Manifest
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.provider.MediaStore
import androidx.activity.result.ActivityResultLauncher
import androidx.core.content.ContextCompat
import com.canhub.cropper.CropImageContractOptions
import com.canhub.cropper.CropImageOptions
import com.canhub.cropper.CropImageView
import network.loki.messenger.R
import org.session.libsession.utilities.getColorFromAttr
import org.session.libsignal.utilities.ExternalStorageUtil.getImageDir
import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.NoExternalStorageException
import org.thoughtcrime.securesms.util.FileProviderUtil
import org.thoughtcrime.securesms.util.IntentUtils
import java.io.File
import java.io.IOException
import java.util.LinkedList
class AvatarSelection(
private val activity: Activity,
private val onAvatarCropped: ActivityResultLauncher<CropImageContractOptions>,
private val onPickImage: ActivityResultLauncher<Intent>
) {
private val TAG: String = AvatarSelection::class.java.simpleName
private val bgColor by lazy { activity.getColorFromAttr(android.R.attr.colorPrimary) }
private val txtColor by lazy { activity.getColorFromAttr(android.R.attr.textColorPrimary) }
private val imageScrim by lazy { ContextCompat.getColor(activity, R.color.avatar_background) }
private val activityTitle by lazy { activity.getString(R.string.CropImageActivity_profile_avatar) }
/**
* Returns result on [.REQUEST_CODE_CROP_IMAGE]
*/
fun circularCropImage(
inputFile: Uri?,
outputFile: Uri?
) {
onAvatarCropped.launch(
CropImageContractOptions(
uri = inputFile,
cropImageOptions = CropImageOptions(
guidelines = CropImageView.Guidelines.ON,
aspectRatioX = 1,
aspectRatioY = 1,
fixAspectRatio = true,
cropShape = CropImageView.CropShape.OVAL,
customOutputUri = outputFile,
allowRotation = true,
allowFlipping = true,
backgroundColor = imageScrim,
toolbarColor = bgColor,
activityBackgroundColor = bgColor,
toolbarTintColor = txtColor,
toolbarBackButtonColor = txtColor,
toolbarTitleColor = txtColor,
activityMenuIconColor = txtColor,
activityMenuTextColor = txtColor,
activityTitle = activityTitle
)
)
)
}
/**
* Returns result on [.REQUEST_CODE_AVATAR]
*
* @return Temporary capture file if created.
*/
fun startAvatarSelection(
includeClear: Boolean,
attemptToIncludeCamera: Boolean
): File? {
var captureFile: File? = null
val hasCameraPermission = ContextCompat
.checkSelfPermission(
activity,
Manifest.permission.CAMERA
) == PackageManager.PERMISSION_GRANTED
if (attemptToIncludeCamera && hasCameraPermission) {
try {
captureFile = File.createTempFile("avatar-capture", ".jpg", getImageDir(activity))
} catch (e: IOException) {
Log.e("Cannot reserve a temporary avatar capture file.", e)
} catch (e: NoExternalStorageException) {
Log.e("Cannot reserve a temporary avatar capture file.", e)
}
}
val chooserIntent = createAvatarSelectionIntent(activity, captureFile, includeClear)
onPickImage.launch(chooserIntent)
return captureFile
}
private fun createAvatarSelectionIntent(
context: Context,
tempCaptureFile: File?,
includeClear: Boolean
): Intent {
val extraIntents: MutableList<Intent> = LinkedList()
var galleryIntent = Intent(Intent.ACTION_PICK)
galleryIntent.setDataAndType(MediaStore.Images.Media.INTERNAL_CONTENT_URI, "image/*")
if (!IntentUtils.isResolvable(context, galleryIntent)) {
galleryIntent = Intent(Intent.ACTION_GET_CONTENT)
galleryIntent.setType("image/*")
}
if (tempCaptureFile != null) {
val uri = FileProviderUtil.getUriFor(context, tempCaptureFile)
val cameraIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, uri)
cameraIntent.setFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
extraIntents.add(cameraIntent)
}
if (includeClear) {
extraIntents.add(Intent("network.loki.securesms.action.CLEAR_PROFILE_PHOTO"))
}
val chooserIntent = Intent.createChooser(
galleryIntent,
context.getString(R.string.CreateProfileActivity_profile_photo)
)
if (!extraIntents.isEmpty()) {
chooserIntent.putExtra(
Intent.EXTRA_INITIAL_INTENTS,
extraIntents.toTypedArray<Intent>()
)
}
return chooserIntent
}
}

View File

@ -18,6 +18,7 @@ import android.view.View
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@ -34,6 +35,8 @@ import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import com.canhub.cropper.CropImage
import com.canhub.cropper.CropImageContract
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.awaitClose
@ -77,12 +80,12 @@ import org.thoughtcrime.securesms.ui.Cell
import org.thoughtcrime.securesms.ui.Divider
import org.thoughtcrime.securesms.ui.LargeItemButton
import org.thoughtcrime.securesms.ui.LargeItemButtonWithDrawable
import org.thoughtcrime.securesms.ui.theme.LocalDimensions
import org.thoughtcrime.securesms.ui.theme.dangerButtonColors
import org.thoughtcrime.securesms.ui.components.PrimaryOutlineButton
import org.thoughtcrime.securesms.ui.components.PrimaryOutlineCopyButton
import org.thoughtcrime.securesms.ui.contentDescription
import org.thoughtcrime.securesms.ui.setThemedContent
import org.thoughtcrime.securesms.ui.theme.LocalDimensions
import org.thoughtcrime.securesms.ui.theme.dangerButtonColors
import org.thoughtcrime.securesms.util.BitmapDecodingException
import org.thoughtcrime.securesms.util.BitmapUtil
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
@ -109,6 +112,48 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
private val hexEncodedPublicKey: String get() = TextSecurePreferences.getLocalNumber(this)!!
private val onAvatarCropped = registerForActivityResult(CropImageContract()) { result ->
when {
result.isSuccessful -> {
Log.i(TAG, result.getUriFilePath(this).toString())
lifecycleScope.launch(Dispatchers.IO) {
try {
val profilePictureToBeUploaded =
BitmapUtil.createScaledBytes(
this@SettingsActivity,
result.getUriFilePath(this@SettingsActivity).toString(),
ProfileMediaConstraints()
).bitmap
launch(Dispatchers.Main) {
updateProfilePicture(profilePictureToBeUploaded)
}
} catch (e: BitmapDecodingException) {
Log.e(TAG, e)
}
}
}
result is CropImage.CancelledResult -> {
Log.i(TAG, "Cropping image was cancelled by the user")
}
else -> {
Log.e(TAG, "Cropping image failed")
}
}
}
private val onPickImage = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
){ result ->
if (result.resultCode != Activity.RESULT_OK) return@registerForActivityResult
val outputFile = Uri.fromFile(File(cacheDir, "cropped"))
val inputFile: Uri? = result.data?.data ?: tempFile?.let(Uri::fromFile)
cropImage(inputFile, outputFile)
}
private val avatarSelection = AvatarSelection(this, onAvatarCropped, onPickImage)
companion object {
private const val SCROLL_STATE = "SCROLL_STATE"
}
@ -181,31 +226,6 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
}
}
@Deprecated("Deprecated in Java")
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (resultCode != Activity.RESULT_OK) return
when (requestCode) {
AvatarSelection.REQUEST_CODE_AVATAR -> {
val outputFile = Uri.fromFile(File(cacheDir, "cropped"))
val inputFile: Uri? = data?.data ?: tempFile?.let(Uri::fromFile)
AvatarSelection.circularCropImage(this, inputFile, outputFile, R.string.CropImageActivity_profile_avatar)
}
AvatarSelection.REQUEST_CODE_CROP_IMAGE -> {
lifecycleScope.launch(Dispatchers.IO) {
try {
val profilePictureToBeUploaded = BitmapUtil.createScaledBytes(this@SettingsActivity, AvatarSelection.getResultUri(data), ProfileMediaConstraints()).bitmap
launch(Dispatchers.Main) {
updateProfilePicture(profilePictureToBeUploaded)
}
} catch (e: BitmapDecodingException) {
Log.e(TAG, e)
}
}
}
}
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults)
@ -407,10 +427,17 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
Permissions.with(this)
.request(Manifest.permission.CAMERA)
.onAnyResult {
tempFile = AvatarSelection.startAvatarSelection(this, false, true)
tempFile = avatarSelection.startAvatarSelection( false, true)
}
.execute()
}
private fun cropImage(inputFile: Uri?, outputFile: Uri?){
avatarSelection.circularCropImage(
inputFile = inputFile,
outputFile = outputFile,
)
}
// endregion
private inner class DisplayNameEditActionModeCallback: ActionMode.Callback {