Merge pull request #1609 from oxen-io/feature/updating-cropping

Feature/updating cropping
This commit is contained in:
ThomasSession 2024-08-09 14:54:57 +10:00 committed by GitHub
commit 90cc704016
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 96 additions and 55 deletions

View File

@ -284,7 +284,7 @@ dependencies {
implementation 'com.pnikosis:materialish-progress:1.5' implementation 'com.pnikosis:materialish-progress:1.5'
implementation 'org.greenrobot:eventbus:3.0.0' implementation 'org.greenrobot:eventbus:3.0.0'
implementation 'pl.tajchert:waitingdots:0.1.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.melnykov:floatingactionbutton:1.3.0'
implementation 'com.google.zxing:android-integration:3.1.0' implementation 'com.google.zxing:android-integration:3.1.0'
implementation 'mobi.upod:time-duration-picker:1.1.3' implementation 'mobi.upod:time-duration-picker:1.1.3'

View File

@ -6,13 +6,14 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.net.Uri import android.net.Uri
import android.os.Build
import android.provider.MediaStore import android.provider.MediaStore
import androidx.annotation.StringRes import androidx.activity.result.ActivityResultLauncher
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import com.theartofdev.edmodo.cropper.CropImage import com.canhub.cropper.CropImageContractOptions
import com.theartofdev.edmodo.cropper.CropImageView import com.canhub.cropper.CropImageOptions
import com.canhub.cropper.CropImageView
import network.loki.messenger.R import network.loki.messenger.R
import org.session.libsession.utilities.getColorFromAttr
import org.session.libsignal.utilities.ExternalStorageUtil.getImageDir import org.session.libsignal.utilities.ExternalStorageUtil.getImageDir
import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.NoExternalStorageException import org.session.libsignal.utilities.NoExternalStorageException
@ -22,35 +23,49 @@ import java.io.File
import java.io.IOException import java.io.IOException
import java.util.LinkedList import java.util.LinkedList
object AvatarSelection { 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 TAG: String = AvatarSelection::class.java.simpleName
const val REQUEST_CODE_CROP_IMAGE: Int = CropImage.CROP_IMAGE_ACTIVITY_REQUEST_CODE private val bgColor by lazy { activity.getColorFromAttr(android.R.attr.colorPrimary) }
const val REQUEST_CODE_AVATAR: Int = REQUEST_CODE_CROP_IMAGE + 1 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] * Returns result on [.REQUEST_CODE_CROP_IMAGE]
*/ */
fun circularCropImage( fun circularCropImage(
activity: Activity,
inputFile: Uri?, inputFile: Uri?,
outputFile: Uri?, outputFile: Uri?
@StringRes title: Int
) { ) {
CropImage.activity(inputFile) onAvatarCropped.launch(
.setGuidelines(CropImageView.Guidelines.ON) CropImageContractOptions(
.setAspectRatio(1, 1) uri = inputFile,
.setCropShape(if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) CropImageView.CropShape.RECTANGLE else CropImageView.CropShape.OVAL) cropImageOptions = CropImageOptions(
.setOutputUri(outputFile) guidelines = CropImageView.Guidelines.ON,
.setAllowRotation(true) aspectRatioX = 1,
.setAllowFlipping(true) aspectRatioY = 1,
.setBackgroundColor(ContextCompat.getColor(activity, R.color.avatar_background)) fixAspectRatio = true,
.setActivityTitle(activity.getString(title)) cropShape = CropImageView.CropShape.OVAL,
.start(activity) customOutputUri = outputFile,
} allowRotation = true,
allowFlipping = true,
fun getResultUri(data: Intent?): Uri { backgroundColor = imageScrim,
return CropImage.getActivityResult(data).uri toolbarColor = bgColor,
activityBackgroundColor = bgColor,
toolbarTintColor = txtColor,
toolbarBackButtonColor = txtColor,
toolbarTitleColor = txtColor,
activityMenuIconColor = txtColor,
activityMenuTextColor = txtColor,
activityTitle = activityTitle
)
)
)
} }
/** /**
@ -59,7 +74,6 @@ object AvatarSelection {
* @return Temporary capture file if created. * @return Temporary capture file if created.
*/ */
fun startAvatarSelection( fun startAvatarSelection(
activity: Activity,
includeClear: Boolean, includeClear: Boolean,
attemptToIncludeCamera: Boolean attemptToIncludeCamera: Boolean
): File? { ): File? {
@ -80,7 +94,7 @@ object AvatarSelection {
} }
val chooserIntent = createAvatarSelectionIntent(activity, captureFile, includeClear) val chooserIntent = createAvatarSelectionIntent(activity, captureFile, includeClear)
activity.startActivityForResult(chooserIntent, REQUEST_CODE_AVATAR) onPickImage.launch(chooserIntent)
return captureFile return captureFile
} }

View File

@ -18,6 +18,7 @@ import android.view.View
import android.view.inputmethod.EditorInfo import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import android.widget.Toast import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.Crossfade import androidx.compose.animation.Crossfade
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@ -34,6 +35,8 @@ import androidx.core.view.isInvisible
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.localbroadcastmanager.content.LocalBroadcastManager import androidx.localbroadcastmanager.content.LocalBroadcastManager
import com.canhub.cropper.CropImage
import com.canhub.cropper.CropImageContract
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.channels.awaitClose
@ -78,12 +81,12 @@ import org.thoughtcrime.securesms.ui.Cell
import org.thoughtcrime.securesms.ui.Divider import org.thoughtcrime.securesms.ui.Divider
import org.thoughtcrime.securesms.ui.LargeItemButton import org.thoughtcrime.securesms.ui.LargeItemButton
import org.thoughtcrime.securesms.ui.LargeItemButtonWithDrawable 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.PrimaryOutlineButton
import org.thoughtcrime.securesms.ui.components.PrimaryOutlineCopyButton import org.thoughtcrime.securesms.ui.components.PrimaryOutlineCopyButton
import org.thoughtcrime.securesms.ui.contentDescription import org.thoughtcrime.securesms.ui.contentDescription
import org.thoughtcrime.securesms.ui.setThemedContent 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.BitmapDecodingException
import org.thoughtcrime.securesms.util.BitmapUtil import org.thoughtcrime.securesms.util.BitmapUtil
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
@ -109,6 +112,48 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
private val hexEncodedPublicKey: String get() = TextSecurePreferences.getLocalNumber(this)!! 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 { companion object {
private const val SCROLL_STATE = "SCROLL_STATE" 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) { override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults) super.onRequestPermissionsResult(requestCode, permissions, grantResults)
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults) Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults)
@ -407,10 +427,17 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
Permissions.with(this) Permissions.with(this)
.request(Manifest.permission.CAMERA) .request(Manifest.permission.CAMERA)
.onAnyResult { .onAnyResult {
tempFile = AvatarSelection.startAvatarSelection(this, false, true) tempFile = avatarSelection.startAvatarSelection( false, true)
} }
.execute() .execute()
} }
private fun cropImage(inputFile: Uri?, outputFile: Uri?){
avatarSelection.circularCropImage(
inputFile = inputFile,
outputFile = outputFile,
)
}
// endregion // endregion
private inner class DisplayNameEditActionModeCallback: ActionMode.Callback { private inner class DisplayNameEditActionModeCallback: ActionMode.Callback {