0
0
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:
lukstbit 2022-05-22 12:05:47 +03:00 committed by Mike Hardy
parent 29b334fbf7
commit 1fabcffcad
4 changed files with 61 additions and 25 deletions

View File

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

View File

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

View File

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

View File

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