mirror of
https://github.com/ankidroid/Anki-Android.git
synced 2024-09-20 03:52:15 +02:00
Update image cropper library dependency to 4.2.1
This update required a refactoring of the code around cropping an image as the newer version of the library relies on the newer ActivityResultLauncher/Contract apis.
This commit is contained in:
parent
29b334fbf7
commit
1fabcffcad
@ -261,7 +261,7 @@ dependencies {
|
||||
// Note: the design support library can be quite buggy, so test everything thoroughly before updating it
|
||||
implementation 'com.google.android.material:material:1.6.0'
|
||||
implementation 'com.fasterxml.jackson.core:jackson-databind:2.13.2.2'
|
||||
implementation 'com.github.CanHub:Android-Image-Cropper:3.1.3'
|
||||
implementation 'com.github.CanHub:Android-Image-Cropper:4.2.1'
|
||||
|
||||
// build with ./gradlew rsdroid:assembleRelease
|
||||
// In my experience, using `files()` currently requires a reindex operation.
|
||||
|
@ -305,7 +305,7 @@
|
||||
</activity>
|
||||
<activity
|
||||
android:name="com.canhub.cropper.CropImageActivity"
|
||||
android:exported="false"
|
||||
android:exported="true"
|
||||
android:configChanges="keyboardHidden|orientation|locale|screenSize"
|
||||
android:theme="@style/Base.Theme.AppCompat" />
|
||||
<activity
|
||||
|
@ -45,17 +45,21 @@ import android.widget.Button
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.ActivityResultRegistry
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.core.content.ContentResolverCompat
|
||||
import androidx.core.content.FileProvider
|
||||
import com.afollestad.materialdialogs.DialogAction
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import com.canhub.cropper.CropImage
|
||||
import com.canhub.cropper.*
|
||||
import com.ichi2.anki.AnkiDroidApp
|
||||
import com.ichi2.anki.CrashReportService
|
||||
import com.ichi2.anki.DrawingActivity
|
||||
import com.ichi2.anki.R
|
||||
import com.ichi2.anki.UIUtils
|
||||
import com.ichi2.anki.multimediacard.activity.MultimediaEditFieldActivity
|
||||
import com.ichi2.annotations.NeedsTest
|
||||
import com.ichi2.compat.CompatHelper
|
||||
import com.ichi2.ui.FixedEditText
|
||||
import com.ichi2.utils.BitmapUtil
|
||||
@ -92,6 +96,9 @@ class BasicImageFieldController : FieldControllerBase(), IFieldController {
|
||||
|
||||
return Math.min(height * 0.4, width * 0.6).toInt()
|
||||
}
|
||||
private lateinit var cropImageRequest: ActivityResultLauncher<CropImageContractOptions>
|
||||
@VisibleForTesting
|
||||
lateinit var registryToUse: ActivityResultRegistry
|
||||
|
||||
override fun loadInstanceState(savedInstanceState: Bundle?) {
|
||||
if (savedInstanceState == null) {
|
||||
@ -183,6 +190,29 @@ class BasicImageFieldController : FieldControllerBase(), IFieldController {
|
||||
layout.addView(mCropButton, ViewGroup.LayoutParams.MATCH_PARENT)
|
||||
}
|
||||
|
||||
override fun setEditingActivity(activity: MultimediaEditFieldActivity) {
|
||||
super.setEditingActivity(activity)
|
||||
val registryToUse = if (this::registryToUse.isInitialized) registryToUse else mActivity.activityResultRegistry
|
||||
@NeedsTest("check the happy/failure path for the crop action")
|
||||
cropImageRequest = registryToUse.register(CROP_IMAGE_LAUNCHER_KEY, CropImageContract()) { cropResult ->
|
||||
if (cropResult.isSuccessful) {
|
||||
mImageFileSizeWarning?.visibility = View.GONE
|
||||
if (cropResult != null) {
|
||||
handleCropResult(cropResult)
|
||||
}
|
||||
setPreviewImage(mViewModel.imagePath, maxImageSize)
|
||||
} else {
|
||||
if (!TextUtils.isEmpty(mPreviousImagePath)) {
|
||||
revertToPreviousImage()
|
||||
}
|
||||
// cropImage can give us more information. Not sure it is actionable so for now just log it.
|
||||
val error: String = cropResult.error?.toString() ?: "Error info not available"
|
||||
Timber.w(error, "cropImage threw an error")
|
||||
CrashReportService.sendExceptionReport(error, "cropImage threw an error")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("UnsupportedChromeOsCameraSystemFeature")
|
||||
private fun canUseCamera(context: Context): Boolean {
|
||||
if (!Permissions.canUseCamera(context)) {
|
||||
@ -333,9 +363,7 @@ class BasicImageFieldController : FieldControllerBase(), IFieldController {
|
||||
Timber.d("Activity was not successful")
|
||||
// Restore the old version of the image if the user cancelled
|
||||
when (requestCode) {
|
||||
ACTIVITY_TAKE_PICTURE,
|
||||
ACTIVITY_CROP_PICTURE,
|
||||
CropImage.CROP_IMAGE_ACTIVITY_REQUEST_CODE ->
|
||||
ACTIVITY_TAKE_PICTURE ->
|
||||
if (!TextUtils.isEmpty(mPreviousImagePath)) {
|
||||
revertToPreviousImage()
|
||||
}
|
||||
@ -346,17 +374,6 @@ class BasicImageFieldController : FieldControllerBase(), IFieldController {
|
||||
if (resultCode >= Activity.RESULT_FIRST_USER) {
|
||||
UIUtils.showThemedToast(mActivity, mActivity.getString(R.string.activity_result_unexpected), true)
|
||||
}
|
||||
|
||||
// cropImage can give us more information. Not sure it is actionable so for now just log it.
|
||||
if (requestCode == CropImage.CROP_IMAGE_ACTIVITY_REQUEST_CODE && resultCode == CropImage.CROP_IMAGE_ACTIVITY_RESULT_ERROR_CODE) {
|
||||
val result: CropImage.ActivityResult? = CropImage.getActivityResult(data)
|
||||
if (result != null) {
|
||||
@KotlinCleanup("try using a kotlin method")
|
||||
val error: String = java.lang.String.valueOf(result.error)
|
||||
Timber.w(error, "cropImage threw an error")
|
||||
CrashReportService.sendExceptionReport(error, "cropImage threw an error")
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@ -378,11 +395,6 @@ class BasicImageFieldController : FieldControllerBase(), IFieldController {
|
||||
// receive image from drawing activity
|
||||
val savedImagePath = data!!.extras!![DrawingActivity.EXTRA_RESULT_WHITEBOARD] as Uri?
|
||||
handleDrawingResult(savedImagePath)
|
||||
} else if (requestCode == ACTIVITY_CROP_PICTURE || requestCode == CropImage.CROP_IMAGE_ACTIVITY_REQUEST_CODE) {
|
||||
val result: CropImage.ActivityResult? = CropImage.getActivityResult(data)
|
||||
if (result != null) {
|
||||
handleCropResult(result)
|
||||
}
|
||||
} else {
|
||||
Timber.w("Unhandled request code: %d", requestCode)
|
||||
return
|
||||
@ -491,6 +503,9 @@ class BasicImageFieldController : FieldControllerBase(), IFieldController {
|
||||
|
||||
override fun onDone() {
|
||||
deletePreviousImage()
|
||||
if (this::cropImageRequest.isInitialized) {
|
||||
cropImageRequest.unregister()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -623,8 +638,14 @@ class BasicImageFieldController : FieldControllerBase(), IFieldController {
|
||||
ret = viewModel.beforeCrop(imagePath, imageUri)
|
||||
setTemporaryMedia(imagePath)
|
||||
Timber.d("requestCrop() destination image has path/uri %s/%s", ret.imagePath, ret.imageUri)
|
||||
|
||||
CropImage.activity(viewModel.imageUri).start(mActivity)
|
||||
if (this::cropImageRequest.isInitialized) {
|
||||
cropImageRequest.launch(
|
||||
CropImageContractOptions(
|
||||
viewModel.imageUri,
|
||||
CropImageOptions()
|
||||
)
|
||||
)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
@ -653,7 +674,7 @@ class BasicImageFieldController : FieldControllerBase(), IFieldController {
|
||||
builder.build().show()
|
||||
}
|
||||
|
||||
private fun handleCropResult(result: CropImage.ActivityResult) {
|
||||
private fun handleCropResult(result: CropImageView.CropResult) {
|
||||
Timber.d("handleCropResult")
|
||||
mViewModel.deleteImagePath()
|
||||
mViewModel = ImageViewModel(result.getUriFilePath(mActivity, true), result.uriContent)
|
||||
@ -813,6 +834,7 @@ class BasicImageFieldController : FieldControllerBase(), IFieldController {
|
||||
private const val ACTIVITY_CROP_PICTURE = 3
|
||||
private const val ACTIVITY_DRAWING = 4
|
||||
private const val IMAGE_SAVE_MAX_WIDTH = 1920
|
||||
private const val CROP_IMAGE_LAUNCHER_KEY = "crop_image_launcher_key"
|
||||
|
||||
/**
|
||||
* Get Uri based on current image path
|
||||
|
@ -17,7 +17,10 @@ package com.ichi2.anki.multimediacard.fields
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import androidx.activity.result.ActivityResultRegistry
|
||||
import androidx.activity.result.contract.ActivityResultContract
|
||||
import androidx.annotation.CheckResult
|
||||
import androidx.core.app.ActivityOptionsCompat
|
||||
import com.ichi2.anki.R
|
||||
import com.ichi2.anki.multimediacard.activity.MultimediaEditFieldActivityTestBase
|
||||
import com.ichi2.testutils.AnkiAssert
|
||||
@ -31,6 +34,7 @@ import org.mockito.kotlin.whenever
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.shadows.ShadowToast
|
||||
import java.io.File
|
||||
import kotlin.test.fail
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
open class BasicImageFieldControllerTest : MultimediaEditFieldActivityTestBase() {
|
||||
@ -90,6 +94,16 @@ open class BasicImageFieldControllerTest : MultimediaEditFieldActivityTestBase()
|
||||
@Test
|
||||
fun invalidImageResultDoesNotCrashController() {
|
||||
val controller = validControllerNoImage
|
||||
controller.registryToUse = object : ActivityResultRegistry() {
|
||||
override fun <I, O> onLaunch(
|
||||
requestCode: Int,
|
||||
contract: ActivityResultContract<I, O>,
|
||||
input: I,
|
||||
options: ActivityOptionsCompat?
|
||||
) {
|
||||
fail("Unexpected access to the activity result registry!")
|
||||
}
|
||||
}
|
||||
val activity = setupActivityMock(controller, controller.getActivity())
|
||||
val mock = MockContentResolver.returningEmptyCursor()
|
||||
whenever(activity.contentResolver).thenReturn(mock)
|
||||
|
Loading…
Reference in New Issue
Block a user