mirror of
https://github.com/oxen-io/session-android.git
synced 2024-11-21 15:05:19 +00:00
Replace image cropping library
This commit is contained in:
parent
62873ee773
commit
26b186452a
@ -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"
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user