From 2f867e993670bb75597d6633284ca616fe045b12 Mon Sep 17 00:00:00 2001 From: Paul Woitaschek Date: Wed, 29 Jun 2022 14:56:49 +0200 Subject: [PATCH] Remove the manifest hack (#399) * Remove the manifest hack. * Uncomment the CropImageContractTest --- build.gradle | 290 ------------ cropper/build.gradle | 5 +- cropper/src/main/AndroidManifest.xml | 26 -- .../canhub/cropper/CropImageContractTest.kt | 426 +++++++++--------- versions.gradle | 5 +- 5 files changed, 221 insertions(+), 531 deletions(-) diff --git a/build.gradle b/build.gradle index 1991aa32..191c3e0f 100644 --- a/build.gradle +++ b/build.gradle @@ -24,293 +24,3 @@ allprojects { maven { url "https://jitpack.io" } } } - - -/** - * For apps targeting Android 12, if the AndroidManifest.xml file contains , , , or - * components that contain (s), it is required that those components explicitly declare the - * `android:exported` attribute (see https://developer.android.com/about/versions/12/behavior-changes-12#exported). - * This file contains gradle task for adding missing `android:exported` attributes to AndroidManifest.xml files. - * - * 1. copy the content of this file to your `build.gradle` file located in your project's root folder. - * 2. in terminal (cmd), in your project's root folder, execute `./gradlew doAddAndroidExportedIfNecessary` - * 3. if your project still fails to build with the same error about missing the `android:exported` attribute, - * check out the [doAddAndroidExportedForDependencies] task - * - * DISCLAIMER: the gradle task is to help you avoid manually adding the `android:exported` attribute. This comes in - * handy for projects with large AndroidManifest.xml file, projects with multiple AndroidManifest.xml files due to - * multiple flavors and/or multiple app modules. At the end, you should always review changes done on your files - * by this gradle task. Check the following links to make sure the added `android:exported` attributes match your - * use cases: - * - https://developer.android.com/guide/topics/manifest/activity-element#exported - * - https://developer.android.com/guide/topics/manifest/activity-alias-element#exported - * - https://developer.android.com/guide/topics/manifest/service-element#exported - * - https://developer.android.com/guide/topics/manifest/receiver-element#exported - * - */ - -import org.w3c.dom.Element -import org.w3c.dom.Node - -import javax.xml.transform.dom.DOMSource -import javax.xml.transform.stream.StreamResult -import javax.xml.transform.TransformerFactory -import javax.xml.transform.Transformer - -/** - * For apps targeting Android 12, if the AndroidManifest.xml file contains , , , or - * components that contain (s), it is required that those components explicitly declare the - * `android:exported` attribute (see https://developer.android.com/about/versions/12/behavior-changes-12#exported). - * - * This function automatically adds the missing `android:exported` attribute to components that require it. Prior to - * Android 12, for , , and components that have (s), if - * the `android:exported` attribute was not set explicitly, the default value would be `true`. The previous statement - * is based on researching documentation on the `android:exported` attribute: - * - https://developer.android.com/guide/topics/manifest/activity-element#exported - * - https://developer.android.com/guide/topics/manifest/activity-alias-element#exported - * - https://developer.android.com/guide/topics/manifest/service-element#exported - * - https://developer.android.com/guide/topics/manifest/receiver-element#exported - * Therefore, for , , and components that have (s), if - * the `android:exported` attribute is missing, this function adds the attribute with default value `true`. - * For known exceptions, set the value to `false`: - * - firebase messaging service: https://firebase.google.com/docs/cloud-messaging/android/client#manifest - * - * @param manifestFile the AndroidManifest.xml file to be investigated - */ -def addAndroidExportedIfNecessary(File manifestFile) { - def manifestAltered = false - def reader = manifestFile.newReader() - def document = groovy.xml.DOMBuilder.parse(reader) - def application = document.getElementsByTagName("application").item(0) - if (application != null) { - println "Searching for activities, services and receivers with intent filters..." - application.childNodes.each { child -> - def childNodeName = child.nodeName - if (childNodeName == "activity" || childNodeName == "activity-alias" || - childNodeName == "service" || childNodeName == "receiver") { - def attributes = child.getAttributes() - if (attributes.getNamedItem("android:exported") == null) { - def intentFilters = child.childNodes.findAll { - it.nodeName == "intent-filter" - } - if (intentFilters.size() > 0) { - println "found ${childNodeName} ${attributes.getNamedItem("android:name").nodeValue} " + - "with intent filters but without android:exported attribute" - - def exportedAttrAdded = false - for (def i = 0; i < intentFilters.size(); i++) { - def intentFilter = intentFilters[i] - def actions = intentFilter.childNodes.findAll { - it.nodeName == "action" - } - for (def j = 0; j < actions.size(); j++) { - def action = actions[j] - def actionName = action.getAttributes().getNamedItem("android:name").nodeValue - if (actionName == "com.google.firebase.MESSAGING_EVENT") { - println "adding exported=false to ${attributes.getNamedItem("android:name")}..." - ((Element) child).setAttribute("android:exported", "false") - manifestAltered = true - exportedAttrAdded = true - } - } - } - if (!exportedAttrAdded) { - println "adding exported=true to ${attributes.getNamedItem("android:name")}..." - ((Element) child).setAttribute("android:exported", "true") - manifestAltered = true - } - } - } - } - } - } - if (manifestAltered) { - document.setXmlStandalone(true) - Transformer transformer = TransformerFactory.newInstance().newTransformer() - DOMSource source = new DOMSource(document) - FileWriter writer = new FileWriter(manifestFile) - StreamResult result = new StreamResult(writer) - transformer.transform(source, result) - println "Done adding missing android:exported attributes to your AndroidManifest.xml. You may want to" + - "additionally prettify it in Android Studio using [command + option + L](mac) or [CTRL+ALT+L](windows)." - } else { - println "Hooray, your AndroidManifest.xml did not need any change." - } -} - -/** - * Given an AndroidManifest.xml file, extract components with missing `android:exported` attribute, also add that - * attribute to those components. - */ -def getMissingAndroidExportedComponents(File manifestFile) { - List nodesFromDependencies = new ArrayList<>() - def reader = manifestFile.newReader() - def document = groovy.xml.DOMBuilder.parse(reader) - def application = document.getElementsByTagName("application").item(0) - if (application != null) { - println "Searching for activities, services and receivers with intent filters..." - application.childNodes.each { child -> - def childNodeName = child.nodeName - if (childNodeName == "activity" || childNodeName == "activity-alias" || - childNodeName == "service" || childNodeName == "receiver") { - def attributes = child.getAttributes() - if (attributes.getNamedItem("android:exported") == null) { - def intentFilters = child.childNodes.findAll { - it.nodeName == "intent-filter" - } - if (intentFilters.size() > 0) { - println "found ${childNodeName} ${attributes.getNamedItem("android:name").nodeValue} " + - "with intent filters but without android:exported attribute" - - def exportedAttrAdded = false - for (def i = 0; i < intentFilters.size(); i++) { - def intentFilter = intentFilters[i] - def actions = intentFilter.childNodes.findAll { - it.nodeName == "action" - } - for (def j = 0; j < actions.size(); j++) { - def action = actions[j] - def actionName = action.getAttributes().getNamedItem("android:name").nodeValue - if (actionName == "com.google.firebase.MESSAGING_EVENT") { - println "adding exported=false to ${attributes.getNamedItem("android:name")}..." - ((Element) child).setAttribute("android:exported", "false") - exportedAttrAdded = true - } - } - } - if (!exportedAttrAdded) { - println "adding exported=true to ${attributes.getNamedItem("android:name")}..." - ((Element) child).setAttribute("android:exported", "true") - } - nodesFromDependencies.add(child) - } - } - } - } - } - return nodesFromDependencies -} - -/** - * Add [components] to the given an AndroidManifest.xml file's component - */ -def addManifestFileComponents(File manifestFile, List components) { - def reader = manifestFile.newReader() - def document = groovy.xml.DOMBuilder.parse(reader) - def application = document.getElementsByTagName("application").item(0) - if (application != null) { - println "Adding missing components with android:exported attribute to ${manifestFile.absolutePath} ..." - components.each { node -> - Node importedNode = document.importNode(node, true) - application.appendChild(importedNode) - } - } - if (components.size() > 0) { - document.setXmlStandalone(true) - Transformer transformer = TransformerFactory.newInstance().newTransformer() - DOMSource source = new DOMSource(document) - FileWriter writer = new FileWriter(manifestFile) - StreamResult result = new StreamResult(writer) - transformer.transform(source, result) - println "Added missing app-dependencies components with android:exported attributes to your " + - "AndroidManifest.xml.You may want to additionally prettify it in Android Studio using " + - "[command + option + L](mac) or [CTRL+ALT+L](windows)." - } - println "----" -} - -task doAddAndroidExportedIfNecessary { - doLast { - def root = new File(project.rootDir, "") - if (root.isDirectory()) { - def children = root.listFiles() - for (def i = 0; i < children.size(); i++) { - File child = children[i] - if (child.isDirectory()) { - File srcDirectory = new File(child, "src") - if (srcDirectory.exists() && srcDirectory.isDirectory()) { - def srcChildren = srcDirectory.listFiles() - for (def j = 0; j < srcChildren.size(); j++) { - File manifestFile = new File(srcChildren[j], "AndroidManifest.xml") - if (manifestFile.exists() && manifestFile.isFile()) { - println "found manifest file: ${manifestFile.absolutePath}" - addAndroidExportedIfNecessary(manifestFile) - println "-----" - } - } - } - } - } - } - } -} - -/** - * If your project has dependency on libraries that haven't updated their AndroidManifest.xml files yet to conform to - * the Android 12 requirement, your app may still fail to build due to missing `android:exported` attributes in those - * libraries' AndroidManifest.xml files, even after running the [doAddAndroidExportedIfNecessary] task. This task - * extracts the components that are missing the `android:exported` attribute from the merged manifest, which includes - * components from imported libraries, then adds the components to the project's AndroidManifest.xml files that contains - * component. The added components should override their declaration in the libraries' manifest files. - * As we cannot modify the libraries' manifest files, this should be an acceptable workaround. - * - * NOTE: always run [doAddAndroidExportedIfNecessary] first before running this task, in order to avoid adding duplicate - * components to the project's AndroidManifest.xml files. After [doAddAndroidExportedIfNecessary] finishes, rebuild your - * project, otherwise the merged manifest won't be created. Only after those steps, execute this task. - * - * NOTE: This task assumes certain structure of the path to the merged manifest, which is created after project - * build. The path structure may be dependent on the gradle version. This task was tested with gradle-6.8 and - * Android Studio Arctic Fox. - * - * NOTE: If your project already targets Android 12 and still contains libraries with missing `android:exported` - * attributes for required components in their AndroidManifest.xml files, your build will fail and the merged manifest - * won't be created. Therefore, call this task before you target Android 12; or: - * - temporarily downgrade the targetSdkVersion (and compileSDKVersion) to 30 - * - run [doAddAndroidExportedIfNecessary] task - * - rebuild your project (to build the merged manifest) - * - run this task - * - set the targetSdkVersion back to target Android 12 - */ -task doAddAndroidExportedForDependencies { - doLast { - List missingComponents = new ArrayList<>() - def root = new File(project.rootDir, "") - if (root.isDirectory()) { - def children = root.listFiles() - for (def i = 0; i < children.size(); i++) { - File child = children[i] - if (child.isDirectory()) { - File mergedManifestsDirectory = new File(child, "build/intermediates/merged_manifests") - if (mergedManifestsDirectory.exists() && mergedManifestsDirectory.isDirectory()) { - def manifestFiles = mergedManifestsDirectory.listFiles().findAll { directoryChild -> - directoryChild.isDirectory() && - (new File(directoryChild, "AndroidManifest.xml")).exists() - }.stream().map { directoryWithManifest -> - new File(directoryWithManifest, "AndroidManifest.xml") - }.toArray() - - if (manifestFiles.size() > 0) { - File mergedManifest = manifestFiles[0] - if (mergedManifest.exists() && mergedManifest.isFile()) { - missingComponents = getMissingAndroidExportedComponents(mergedManifest) - - if (missingComponents.size() > 0) { - File srcDirectory = new File(child, "src") - if (srcDirectory.exists() && srcDirectory.isDirectory()) { - def srcChildren = srcDirectory.listFiles() - for (def j = 0; j < srcChildren.size(); j++) { - File manifestFile = new File(srcChildren[j], "AndroidManifest.xml") - if (manifestFile.exists() && manifestFile.isFile()) { - addManifestFileComponents(manifestFile, missingComponents) - } - } - } - } - } - } - } - } - } - } - } -} \ No newline at end of file diff --git a/cropper/build.gradle b/cropper/build.gradle index 945c8b6c..02ab4643 100644 --- a/cropper/build.gradle +++ b/cropper/build.gradle @@ -3,7 +3,7 @@ apply plugin: 'kotlin-android' apply plugin: 'kotlin-parcelize' apply plugin: 'maven-publish' -group='com.canhub.cropper' +group = 'com.canhub.cropper' // Because the components are created only during the afterEvaluate phase, you must // configure your publications using the afterEvaluate() lifecycle method. @@ -69,4 +69,7 @@ dependencies { testImplementation "androidx.test:core:$androidXTestVersion" testImplementation "androidx.test:runner:$androidXTestVersion" testImplementation "io.mockk:mockk:$mockkVersion" + + testImplementation "androidx.fragment:fragment-testing:$androidXFragmentTestingVersion" + testImplementation "org.robolectric:robolectric:$robolectricVersion" } diff --git a/cropper/src/main/AndroidManifest.xml b/cropper/src/main/AndroidManifest.xml index 147f2168..2fe21d67 100644 --- a/cropper/src/main/AndroidManifest.xml +++ b/cropper/src/main/AndroidManifest.xml @@ -24,31 +24,5 @@ - - - - - - - - - - - - - - - - - diff --git a/cropper/src/test/java/com/canhub/cropper/CropImageContractTest.kt b/cropper/src/test/java/com/canhub/cropper/CropImageContractTest.kt index dc664e7c..0594c1b1 100644 --- a/cropper/src/test/java/com/canhub/cropper/CropImageContractTest.kt +++ b/cropper/src/test/java/com/canhub/cropper/CropImageContractTest.kt @@ -1,214 +1,214 @@ package com.canhub.cropper -// -// import android.app.Activity -// import android.content.Intent -// import android.graphics.Bitmap -// import android.graphics.Color -// import android.graphics.Rect -// import androidx.activity.result.ActivityResultRegistry -// import androidx.activity.result.contract.ActivityResultContract -// import androidx.core.app.ActivityOptionsCompat -// import androidx.core.net.toUri -// import androidx.fragment.app.testing.launchFragmentInContainer -// import androidx.test.ext.junit.runners.AndroidJUnit4 -// import org.junit.Assert.assertEquals -// import org.junit.Test -// import org.junit.runner.RunWith -// -// @RunWith(AndroidJUnit4::class) -// class CropImageContractTest { -// -// @Test -// fun `WHEN providing invalid options THEN cropping should crash`() { -// // GIVEN -// var result: Exception? = null -// val expected: IllegalArgumentException = IllegalArgumentException() -// var fragment: ContractTestFragment? = null -// val testRegistry = object : ActivityResultRegistry() { -// override fun onLaunch( -// requestCode: Int, -// contract: ActivityResultContract, -// input: I, -// options: ActivityOptionsCompat? -// ) { -// dispatchResult(requestCode, Activity.RESULT_CANCELED, Intent()) -// } -// } -// with(launchFragmentInContainer { ContractTestFragment(testRegistry) }) { -// onFragment { fragment = it } -// } -// // WHEN -// try { -// fragment?.cropImageIntent(options { setMaxZoom(-10) }) -// } catch (e: Exception) { -// result = e -// } -// // THEN -// assertEquals(expected.javaClass, result?.javaClass) -// } -// -// @Test -// fun `WHEN cropping is cancelled by user, THEN result should be cancelled`() { -// // GIVEN -// val expected = CropImage.CancelledResult -// var fragment: ContractTestFragment? = null -// val testRegistry = object : ActivityResultRegistry() { -// override fun onLaunch( -// requestCode: Int, -// contract: ActivityResultContract, -// input: I, -// options: ActivityOptionsCompat? -// ) { -// dispatchResult(requestCode, Activity.RESULT_CANCELED, Intent()) -// } -// } -// with(launchFragmentInContainer { ContractTestFragment(testRegistry) }) { -// onFragment { fragment = it } -// } -// // WHEN -// fragment?.cropImage(options()) -// // THEN -// assertEquals(expected, fragment?.cropResult) -// } -// -// @Test -// fun `WHEN cropping succeeds, THEN result should be successful`() { -// // GIVEN -// var fragment: ContractTestFragment? = null -// val expected = CropImage.ActivityResult( -// originalUri = "content://original".toUri(), -// uriContent = "content://content".toUri(), -// error = null, -// cropPoints = floatArrayOf(), -// cropRect = Rect(1, 2, 3, 4), -// rotation = 45, -// wholeImageRect = Rect(10, 20, 0, 0), -// sampleSize = 0 -// ) -// val testRegistry = object : ActivityResultRegistry() { -// override fun onLaunch( -// requestCode: Int, -// contract: ActivityResultContract, -// input: I, -// options: ActivityOptionsCompat? -// ) { -// val intent = Intent() -// intent.putExtra(CropImage.CROP_IMAGE_EXTRA_RESULT, expected) -// -// dispatchResult(requestCode, CropImage.CROP_IMAGE_ACTIVITY_RESULT_ERROR_CODE, intent) -// } -// } -// with(launchFragmentInContainer { ContractTestFragment(testRegistry) }) { -// onFragment { fragment = it } -// } -// // WHEN -// fragment?.cropImage(options()) -// // THEN -// assertEquals(expected, fragment?.cropResult) -// } -// -// @Test -// fun `WHEN starting crop with all options, THEN intent should contain these options`() { -// // GIVEN -// var cropImageIntent: Intent? = null -// val expectedClassName = CropImageActivity::class.java.name -// val expectedSource = "file://testInput".toUri() -// val options = options(expectedSource) { -// setCropShape(CropImageView.CropShape.OVAL) -// setSnapRadius(1f) -// setTouchRadius(2f) -// setGuidelines(CropImageView.Guidelines.ON_TOUCH) -// setScaleType(CropImageView.ScaleType.CENTER) -// setShowCropOverlay(true) -// setAutoZoomEnabled(false) -// setMultiTouchEnabled(true) -// setCenterMoveEnabled(false) -// setMaxZoom(17) -// setInitialCropWindowPaddingRatio(0.2f) -// setFixAspectRatio(true) -// setAspectRatio(3, 4) -// setBorderLineThickness(3f) -// setBorderLineColor(Color.GREEN) -// setBorderCornerThickness(5f) -// setBorderCornerOffset(6f) -// setBorderCornerLength(7f) -// setBorderCornerColor(Color.MAGENTA) -// setGuidelinesThickness(8f) -// setGuidelinesColor(Color.RED) -// setBackgroundColor(Color.BLUE) -// setMinCropWindowSize(5, 5) -// setMinCropResultSize(10, 10) -// setMaxCropResultSize(5000, 5000) -// setActivityTitle("Test Activity Title") -// setActivityMenuIconColor(Color.BLACK) -// setOutputUri(expectedSource) -// setOutputCompressFormat(Bitmap.CompressFormat.JPEG) -// setOutputCompressQuality(85) -// setRequestedSize(25, 30, CropImageView.RequestSizeOptions.NONE) -// setNoOutputImage(false) -// setInitialCropWindowRectangle(Rect(4, 5, 6, 7)) -// setInitialRotation(13) -// setAllowRotation(true) -// setAllowFlipping(false) -// setAllowCounterRotation(true) -// setRotationDegrees(4) -// setFlipHorizontally(true) -// setFlipVertically(false) -// setCropMenuCropButtonTitle("Test Button Title") -// setCropMenuCropButtonIcon(R.drawable.ic_rotate_left_24) -// } -// val testRegistry = object : ActivityResultRegistry() { -// override fun onLaunch( -// requestCode: Int, -// contract: ActivityResultContract, -// input: I, -// options: ActivityOptionsCompat? -// ) { -// } -// } -// // WHEN -// with(launchFragmentInContainer { ContractTestFragment(testRegistry) }) { -// onFragment { fragment -> cropImageIntent = fragment.cropImageIntent(options) } -// } -// val bundle = cropImageIntent?.getBundleExtra(CropImage.CROP_IMAGE_EXTRA_BUNDLE) -// // THEN -// assertEquals(expectedClassName, cropImageIntent?.component?.className) -// assertEquals(expectedSource, bundle?.getParcelable(CropImage.CROP_IMAGE_EXTRA_SOURCE)) -// } -// -// @Test -// fun `WHEN cropping fails, THEN result should be unsuccessful`() { -// // GIVEN -// var fragment: ContractTestFragment? = null -// val testRegistry = object : ActivityResultRegistry() { -// override fun onLaunch( -// requestCode: Int, -// contract: ActivityResultContract, -// input: I, -// options: ActivityOptionsCompat? -// ) { -// val result = CropImage.ActivityResult( -// originalUri = null, -// uriContent = null, -// error = Exception("Error!"), -// cropPoints = floatArrayOf(), -// cropRect = null, -// rotation = 0, -// wholeImageRect = null, -// sampleSize = 0 -// ) -// val intent = Intent() -// intent.putExtra(CropImage.CROP_IMAGE_EXTRA_RESULT, result) -// -// dispatchResult(requestCode, CropImage.CROP_IMAGE_ACTIVITY_RESULT_ERROR_CODE, intent) -// } -// } -// with(launchFragmentInContainer { ContractTestFragment(testRegistry) }) { -// onFragment { fragment = it } -// } -// // WHEN -// fragment?.cropImage(options()) -// // THEN -// assertEquals(false, fragment?.cropResult?.isSuccessful) -// } -// } + +import android.app.Activity +import android.content.Intent +import android.graphics.Bitmap +import android.graphics.Color +import android.graphics.Rect +import androidx.activity.result.ActivityResultRegistry +import androidx.activity.result.contract.ActivityResultContract +import androidx.core.app.ActivityOptionsCompat +import androidx.core.net.toUri +import androidx.fragment.app.testing.launchFragmentInContainer +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class CropImageContractTest { + + @Test + fun `WHEN providing invalid options THEN cropping should crash`() { + // GIVEN + var result: Exception? = null + val expected: IllegalArgumentException = IllegalArgumentException() + var fragment: ContractTestFragment? = null + val testRegistry = object : ActivityResultRegistry() { + override fun onLaunch( + requestCode: Int, + contract: ActivityResultContract, + input: I, + options: ActivityOptionsCompat? + ) { + dispatchResult(requestCode, Activity.RESULT_CANCELED, Intent()) + } + } + with(launchFragmentInContainer { ContractTestFragment(testRegistry) }) { + onFragment { fragment = it } + } + // WHEN + try { + fragment?.cropImageIntent(options { setMaxZoom(-10) }) + } catch (e: Exception) { + result = e + } + // THEN + assertEquals(expected.javaClass, result?.javaClass) + } + + @Test + fun `WHEN cropping is cancelled by user, THEN result should be cancelled`() { + // GIVEN + val expected = CropImage.CancelledResult + var fragment: ContractTestFragment? = null + val testRegistry = object : ActivityResultRegistry() { + override fun onLaunch( + requestCode: Int, + contract: ActivityResultContract, + input: I, + options: ActivityOptionsCompat? + ) { + dispatchResult(requestCode, Activity.RESULT_CANCELED, Intent()) + } + } + with(launchFragmentInContainer { ContractTestFragment(testRegistry) }) { + onFragment { fragment = it } + } + // WHEN + fragment?.cropImage(options()) + // THEN + assertEquals(expected, fragment?.cropResult) + } + + @Test + fun `WHEN cropping succeeds, THEN result should be successful`() { + // GIVEN + var fragment: ContractTestFragment? = null + val expected = CropImage.ActivityResult( + originalUri = "content://original".toUri(), + uriContent = "content://content".toUri(), + error = null, + cropPoints = floatArrayOf(), + cropRect = Rect(1, 2, 3, 4), + rotation = 45, + wholeImageRect = Rect(10, 20, 0, 0), + sampleSize = 0 + ) + val testRegistry = object : ActivityResultRegistry() { + override fun onLaunch( + requestCode: Int, + contract: ActivityResultContract, + input: I, + options: ActivityOptionsCompat? + ) { + val intent = Intent() + intent.putExtra(CropImage.CROP_IMAGE_EXTRA_RESULT, expected) + + dispatchResult(requestCode, CropImage.CROP_IMAGE_ACTIVITY_RESULT_ERROR_CODE, intent) + } + } + with(launchFragmentInContainer { ContractTestFragment(testRegistry) }) { + onFragment { fragment = it } + } + // WHEN + fragment?.cropImage(options()) + // THEN + assertEquals(expected, fragment?.cropResult) + } + + @Test + fun `WHEN starting crop with all options, THEN intent should contain these options`() { + // GIVEN + var cropImageIntent: Intent? = null + val expectedClassName = CropImageActivity::class.java.name + val expectedSource = "file://testInput".toUri() + val options = options(expectedSource) { + setCropShape(CropImageView.CropShape.OVAL) + setSnapRadius(1f) + setTouchRadius(2f) + setGuidelines(CropImageView.Guidelines.ON_TOUCH) + setScaleType(CropImageView.ScaleType.CENTER) + setShowCropOverlay(true) + setAutoZoomEnabled(false) + setMultiTouchEnabled(true) + setCenterMoveEnabled(false) + setMaxZoom(17) + setInitialCropWindowPaddingRatio(0.2f) + setFixAspectRatio(true) + setAspectRatio(3, 4) + setBorderLineThickness(3f) + setBorderLineColor(Color.GREEN) + setBorderCornerThickness(5f) + setBorderCornerOffset(6f) + setBorderCornerLength(7f) + setBorderCornerColor(Color.MAGENTA) + setGuidelinesThickness(8f) + setGuidelinesColor(Color.RED) + setBackgroundColor(Color.BLUE) + setMinCropWindowSize(5, 5) + setMinCropResultSize(10, 10) + setMaxCropResultSize(5000, 5000) + setActivityTitle("Test Activity Title") + setActivityMenuIconColor(Color.BLACK) + setOutputUri(expectedSource) + setOutputCompressFormat(Bitmap.CompressFormat.JPEG) + setOutputCompressQuality(85) + setRequestedSize(25, 30, CropImageView.RequestSizeOptions.NONE) + setNoOutputImage(false) + setInitialCropWindowRectangle(Rect(4, 5, 6, 7)) + setInitialRotation(13) + setAllowRotation(true) + setAllowFlipping(false) + setAllowCounterRotation(true) + setRotationDegrees(4) + setFlipHorizontally(true) + setFlipVertically(false) + setCropMenuCropButtonTitle("Test Button Title") + setCropMenuCropButtonIcon(R.drawable.ic_rotate_left_24) + } + val testRegistry = object : ActivityResultRegistry() { + override fun onLaunch( + requestCode: Int, + contract: ActivityResultContract, + input: I, + options: ActivityOptionsCompat? + ) { + } + } + // WHEN + with(launchFragmentInContainer { ContractTestFragment(testRegistry) }) { + onFragment { fragment -> cropImageIntent = fragment.cropImageIntent(options) } + } + val bundle = cropImageIntent?.getBundleExtra(CropImage.CROP_IMAGE_EXTRA_BUNDLE) + // THEN + assertEquals(expectedClassName, cropImageIntent?.component?.className) + assertEquals(expectedSource, bundle?.getParcelable(CropImage.CROP_IMAGE_EXTRA_SOURCE)) + } + + @Test + fun `WHEN cropping fails, THEN result should be unsuccessful`() { + // GIVEN + var fragment: ContractTestFragment? = null + val testRegistry = object : ActivityResultRegistry() { + override fun onLaunch( + requestCode: Int, + contract: ActivityResultContract, + input: I, + options: ActivityOptionsCompat? + ) { + val result = CropImage.ActivityResult( + originalUri = null, + uriContent = null, + error = Exception("Error!"), + cropPoints = floatArrayOf(), + cropRect = null, + rotation = 0, + wholeImageRect = null, + sampleSize = 0 + ) + val intent = Intent() + intent.putExtra(CropImage.CROP_IMAGE_EXTRA_RESULT, result) + + dispatchResult(requestCode, CropImage.CROP_IMAGE_ACTIVITY_RESULT_ERROR_CODE, intent) + } + } + with(launchFragmentInContainer { ContractTestFragment(testRegistry) }) { + onFragment { fragment = it } + } + // WHEN + fragment?.cropImage(options()) + // THEN + assertEquals(false, fragment?.cropResult?.isSuccessful) + } +} diff --git a/versions.gradle b/versions.gradle index 59b4a3c3..9de38791 100644 --- a/versions.gradle +++ b/versions.gradle @@ -24,11 +24,14 @@ ext { androidXExifVersion = '1.3.3' androidXCoreKtxVersion = '1.8.0' androidXJunitVersion = '1.1.3' - androidXTestVersion = '1.4.0' // when update to android 12, please uncomment PickImageContractTest && CropImageContractTest + androidXTestVersion = '1.4.0' + androidXFragmentTestingVersion = '1.4.1' // JUnit junitVersion = '4.13.2' // Mockk mockkVersion = '1.12.4' + + robolectricVersion = '4.8.1' }