From 325d7af790e478fb0794acd91948755881aac95c Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Tue, 21 Feb 2023 15:55:12 +0100 Subject: [PATCH 1/6] Updated exif --- buildSrc/src/main/java/dependencies/Dependencies.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildSrc/src/main/java/dependencies/Dependencies.kt b/buildSrc/src/main/java/dependencies/Dependencies.kt index d5fb02c7909..bc34ce13a45 100644 --- a/buildSrc/src/main/java/dependencies/Dependencies.kt +++ b/buildSrc/src/main/java/dependencies/Dependencies.kt @@ -17,7 +17,7 @@ object Dependencies { const val androidx_appcompat = "androidx.appcompat:appcompat:1.5.1" const val androidx_work_runtime = "androidx.work:work-runtime:${Versions.work}" const val androidx_cardview = "androidx.cardview:cardview:1.0.0" - const val androidx_exinterface = "androidx.exifinterface:exifinterface:1.3.1" // Check if https://github.com/getodk/collect/issues/4819 and https://github.com/getodk/collect/issues/5033 no longer takes place before upgrading + const val androidx_exinterface = "androidx.exifinterface:exifinterface:1.3.6" const val androidx_multidex = "androidx.multidex:multidex:2.0.1" const val androidx_preference_ktx = "androidx.preference:preference-ktx:1.2.0" const val androidx_fragment_ktx = "androidx.fragment:fragment-ktx:${Versions.androidx_fragment}" From a35c3614586b41f26349020c091ff2b2154691f0 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Tue, 21 Feb 2023 15:55:52 +0100 Subject: [PATCH 2/6] Copy only some selected exif attributes --- .../android/utilities/ImageConverter.kt | 73 +++++++++++++++---- 1 file changed, 59 insertions(+), 14 deletions(-) diff --git a/collect_app/src/main/java/org/odk/collect/android/utilities/ImageConverter.kt b/collect_app/src/main/java/org/odk/collect/android/utilities/ImageConverter.kt index 751b552bd85..458192b654b 100644 --- a/collect_app/src/main/java/org/odk/collect/android/utilities/ImageConverter.kt +++ b/collect_app/src/main/java/org/odk/collect/android/utilities/ImageConverter.kt @@ -22,7 +22,6 @@ import androidx.exifinterface.media.ExifInterface import org.odk.collect.android.R import org.odk.collect.android.widgets.QuestionWidget import timber.log.Timber -import java.io.IOException object ImageConverter { /** @@ -36,20 +35,9 @@ object ImageConverter { context: Context, imageSizeMode: String ) { - var exif: ExifInterface? = null - try { - exif = ExifInterface(imagePath) - } catch (e: IOException) { - Timber.w(e) - } + backupExifData(imagePath) scaleDownImageIfNeeded(imagePath, questionWidget, context, imageSizeMode) - if (exif != null) { - try { - exif.saveAttributes() - } catch (e: IOException) { - Timber.w(e) - } - } + restoreExifData(imagePath) } private fun scaleDownImageIfNeeded( @@ -112,4 +100,61 @@ object ImageConverter { } } } + + private fun backupExifData(imagePath: String) { + try { + val exif = ExifInterface(imagePath) + for ((key, _) in exifDataBackup) { + exifDataBackup[key] = exif.getAttribute(key) + } + } catch (e: Throwable) { + Timber.w(e) + } + } + + private fun restoreExifData(imagePath: String) { + try { + val exif = ExifInterface(imagePath) + for ((key, value) in exifDataBackup) { + exif.setAttribute(key, value) + } + exif.saveAttributes() + } catch (e: Throwable) { + Timber.w(e) + } + } + + private val exifDataBackup = mutableMapOf( + ExifInterface.TAG_DATETIME to null, + ExifInterface.TAG_DATETIME_ORIGINAL to null, + ExifInterface.TAG_DATETIME_DIGITIZED to null, + ExifInterface.TAG_OFFSET_TIME to null, + ExifInterface.TAG_OFFSET_TIME_ORIGINAL to null, + ExifInterface.TAG_OFFSET_TIME_DIGITIZED to null, + ExifInterface.TAG_SUBSEC_TIME to null, + ExifInterface.TAG_SUBSEC_TIME_ORIGINAL to null, + ExifInterface.TAG_SUBSEC_TIME_DIGITIZED to null, + ExifInterface.TAG_IMAGE_DESCRIPTION to null, + ExifInterface.TAG_MAKE to null, + ExifInterface.TAG_MODEL to null, + ExifInterface.TAG_SOFTWARE to null, + ExifInterface.TAG_ARTIST to null, + ExifInterface.TAG_COPYRIGHT to null, + ExifInterface.TAG_MAKER_NOTE to null, + ExifInterface.TAG_USER_COMMENT to null, + ExifInterface.TAG_IMAGE_UNIQUE_ID to null, + ExifInterface.TAG_CAMERA_OWNER_NAME to null, + ExifInterface.TAG_BODY_SERIAL_NUMBER to null, + ExifInterface.TAG_GPS_ALTITUDE to null, + ExifInterface.TAG_GPS_ALTITUDE_REF to null, + ExifInterface.TAG_GPS_DATESTAMP to null, + ExifInterface.TAG_GPS_TIMESTAMP to null, + ExifInterface.TAG_GPS_LATITUDE to null, + ExifInterface.TAG_GPS_LATITUDE_REF to null, + ExifInterface.TAG_GPS_LONGITUDE to null, + ExifInterface.TAG_GPS_LONGITUDE_REF to null, + ExifInterface.TAG_GPS_SATELLITES to null, + ExifInterface.TAG_GPS_STATUS to null, + ExifInterface.TAG_ORIENTATION to null + ) } From d670e30ca86d2bf8e3fd264f89f6659e8b5be29c Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Fri, 24 Feb 2023 00:31:23 +0100 Subject: [PATCH 3/6] Cleaned and improved ImageConverterTest --- .../utilities/ImageConverterTest.kt | 323 ++++++++---------- 1 file changed, 148 insertions(+), 175 deletions(-) diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/instrumented/utilities/ImageConverterTest.kt b/collect_app/src/androidTest/java/org/odk/collect/android/instrumented/utilities/ImageConverterTest.kt index 10617bd61c6..46e7652a33e 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/instrumented/utilities/ImageConverterTest.kt +++ b/collect_app/src/androidTest/java/org/odk/collect/android/instrumented/utilities/ImageConverterTest.kt @@ -18,76 +18,43 @@ package org.odk.collect.android.instrumented.utilities import android.content.Context import android.graphics.Bitmap import android.graphics.BitmapFactory -import android.media.ExifInterface +import androidx.exifinterface.media.ExifInterface import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 -import org.hamcrest.CoreMatchers.`is` import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.equalTo import org.javarosa.core.model.instance.TreeElement import org.javarosa.form.api.FormEntryPrompt -import org.junit.Assert.assertEquals -import org.junit.Assert.assertNotNull -import org.junit.Before -import org.junit.Rule import org.junit.Test -import org.junit.rules.RuleChain import org.junit.runner.RunWith import org.mockito.Mockito.`when` import org.mockito.Mockito.mock -import org.odk.collect.android.TestSettingsProvider.getUnprotectedSettings -import org.odk.collect.android.injection.DaggerUtils -import org.odk.collect.android.storage.StoragePathProvider -import org.odk.collect.android.storage.StorageSubdirectory -import org.odk.collect.android.support.rules.RunnableRule -import org.odk.collect.android.support.rules.TestRuleChain import org.odk.collect.android.utilities.ApplicationConstants.Namespaces import org.odk.collect.android.utilities.ImageConverter import org.odk.collect.android.utilities.ImageFileUtils import org.odk.collect.android.widgets.ImageWidget -import org.odk.collect.projects.Project import timber.log.Timber import java.io.File import java.io.IOException -import java.lang.Exception -import java.util.HashMap @RunWith(AndroidJUnit4::class) class ImageConverterTest { private lateinit var testImagePath: String - private val generalSettings = getUnprotectedSettings() private val context = ApplicationProvider.getApplicationContext() - @get:Rule - var copyFormChain: RuleChain = TestRuleChain.chain() - .around( - RunnableRule { - // Set up demo project - val component = DaggerUtils.getComponent(ApplicationProvider.getApplicationContext()) - component.projectsRepository().save(Project.DEMO_PROJECT) - component.currentProjectProvider().setCurrentProject(Project.DEMO_PROJECT_ID) - } - ) - - @Before - fun setUp() { - testImagePath = - StoragePathProvider().getOdkDirPath(StorageSubdirectory.INSTANCES) + File.separator + "testForm_2017-10-12_19-36-15" + File.separator + "testImage.jpg" - File(testImagePath).parentFile.mkdirs() - } - @Test fun executeConversionWithoutAnySettings() { - generalSettings.save("image_size", "original_image_size") saveTestBitmap(3000, 3000) ImageConverter.execute(testImagePath, getTestImageWidget(), context, IMAGE_SIZE_ORIGINAL) - val image = ImageFileUtils.getBitmap(testImagePath, BitmapFactory.Options()) - assertEquals(3000, image!!.width) - assertEquals(3000, image.height) + + val image = ImageFileUtils.getBitmap(testImagePath, BitmapFactory.Options())!! + + assertThat(3000, equalTo(image.width)) + assertThat(3000, equalTo(image.height)) } @Test fun scaleImageDownFormLevelOnly1() { - generalSettings.save("image_size", "original_image_size") saveTestBitmap(4000, 3000) ImageConverter.execute( testImagePath, @@ -95,14 +62,15 @@ class ImageConverterTest { context, IMAGE_SIZE_ORIGINAL ) - val image = ImageFileUtils.getBitmap(testImagePath, BitmapFactory.Options()) - assertEquals(2000, image!!.width) - assertEquals(1500, image.height) + + val image = ImageFileUtils.getBitmap(testImagePath, BitmapFactory.Options())!! + + assertThat(2000, equalTo(image.width)) + assertThat(1500, equalTo(image.height)) } @Test fun scaleImageDownFormLevelOnly2() { - generalSettings.save("image_size", "original_image_size") saveTestBitmap(3000, 4000) ImageConverter.execute( testImagePath, @@ -110,14 +78,15 @@ class ImageConverterTest { context, IMAGE_SIZE_ORIGINAL ) - val image = ImageFileUtils.getBitmap(testImagePath, BitmapFactory.Options()) - assertEquals(1500, image!!.width) - assertEquals(2000, image.height) + + val image = ImageFileUtils.getBitmap(testImagePath, BitmapFactory.Options())!! + + assertThat(1500, equalTo(image.width)) + assertThat(2000, equalTo(image.height)) } @Test fun scaleImageDownFormLevelOnly3() { - generalSettings.save("image_size", "original_image_size") saveTestBitmap(3000, 3000) ImageConverter.execute( testImagePath, @@ -125,14 +94,15 @@ class ImageConverterTest { context, IMAGE_SIZE_ORIGINAL ) - val image = ImageFileUtils.getBitmap(testImagePath, BitmapFactory.Options()) - assertEquals(2000, image!!.width) - assertEquals(2000, image.height) + + val image = ImageFileUtils.getBitmap(testImagePath, BitmapFactory.Options())!! + + assertThat(2000, equalTo(image.width)) + assertThat(2000, equalTo(image.height)) } @Test fun scaleImageDownFormLevelOnly4() { - generalSettings.save("image_size", "original_image_size") saveTestBitmap(3000, 3000) ImageConverter.execute( testImagePath, @@ -140,14 +110,15 @@ class ImageConverterTest { context, IMAGE_SIZE_ORIGINAL ) - val image = ImageFileUtils.getBitmap(testImagePath, BitmapFactory.Options()) - assertEquals(3000, image!!.width) - assertEquals(3000, image.height) + + val image = ImageFileUtils.getBitmap(testImagePath, BitmapFactory.Options())!! + + assertThat(3000, equalTo(image.width)) + assertThat(3000, equalTo(image.height)) } @Test fun scaleImageDownFormLevelOnly5() { - generalSettings.save("image_size", "original_image_size") saveTestBitmap(3000, 3000) ImageConverter.execute( testImagePath, @@ -155,14 +126,15 @@ class ImageConverterTest { context, IMAGE_SIZE_ORIGINAL ) - val image = ImageFileUtils.getBitmap(testImagePath, BitmapFactory.Options()) - assertEquals(3000, image!!.width) - assertEquals(3000, image.height) + + val image = ImageFileUtils.getBitmap(testImagePath, BitmapFactory.Options())!! + + assertThat(3000, equalTo(image.width)) + assertThat(3000, equalTo(image.height)) } @Test fun scaleImageDownFormLevelOnly6() { - generalSettings.save("image_size", "original_image_size") saveTestBitmap(3000, 3000) ImageConverter.execute( testImagePath, @@ -170,14 +142,15 @@ class ImageConverterTest { context, IMAGE_SIZE_ORIGINAL ) - val image = ImageFileUtils.getBitmap(testImagePath, BitmapFactory.Options()) - assertEquals(2998, image!!.width) - assertEquals(2998, image.height) + + val image = ImageFileUtils.getBitmap(testImagePath, BitmapFactory.Options())!! + + assertThat(2998, equalTo(image.width)) + assertThat(2998, equalTo(image.height)) } @Test fun scaleImageDownFormLevelOnly7() { - generalSettings.save("image_size", "original_image_size") saveTestBitmap(3000, 3000) ImageConverter.execute( testImagePath, @@ -185,14 +158,15 @@ class ImageConverterTest { context, IMAGE_SIZE_ORIGINAL ) - val image = ImageFileUtils.getBitmap(testImagePath, BitmapFactory.Options()) - assertEquals(3000, image!!.width) - assertEquals(3000, image.height) + + val image = ImageFileUtils.getBitmap(testImagePath, BitmapFactory.Options())!! + + assertThat(3000, equalTo(image.width)) + assertThat(3000, equalTo(image.height)) } @Test fun scaleImageDownFormLevelOnly8() { - generalSettings.save("image_size", "original_image_size") saveTestBitmap(3000, 3000) ImageConverter.execute( testImagePath, @@ -200,14 +174,15 @@ class ImageConverterTest { context, IMAGE_SIZE_ORIGINAL ) - val image = ImageFileUtils.getBitmap(testImagePath, BitmapFactory.Options()) - assertEquals(3000, image!!.width) - assertEquals(3000, image.height) + + val image = ImageFileUtils.getBitmap(testImagePath, BitmapFactory.Options())!! + + assertThat(3000, equalTo(image.width)) + assertThat(3000, equalTo(image.height)) } @Test fun scaleImageDownFormLevelOnly9() { - generalSettings.save("image_size", "original_image_size") saveTestBitmap(3000, 3000) ImageConverter.execute( testImagePath, @@ -215,14 +190,15 @@ class ImageConverterTest { context, IMAGE_SIZE_ORIGINAL ) - val image = ImageFileUtils.getBitmap(testImagePath, BitmapFactory.Options()) - assertEquals(3000, image!!.width) - assertEquals(3000, image.height) + + val image = ImageFileUtils.getBitmap(testImagePath, BitmapFactory.Options())!! + + assertThat(3000, equalTo(image.width)) + assertThat(3000, equalTo(image.height)) } @Test fun scaleImageDownFormLevelOnly10() { - generalSettings.save("image_size", "original_image_size") saveTestBitmap(3000, 3000) ImageConverter.execute( testImagePath, @@ -230,14 +206,15 @@ class ImageConverterTest { context, IMAGE_SIZE_ORIGINAL ) - val image = ImageFileUtils.getBitmap(testImagePath, BitmapFactory.Options()) - assertEquals(3000, image!!.width) - assertEquals(3000, image.height) + + val image = ImageFileUtils.getBitmap(testImagePath, BitmapFactory.Options())!! + + assertThat(3000, equalTo(image.width)) + assertThat(3000, equalTo(image.height)) } @Test fun scaleImageDownFormLevelOnly11() { - generalSettings.save("image_size", "original_image_size") saveTestBitmap(3000, 3000) ImageConverter.execute( testImagePath, @@ -245,14 +222,15 @@ class ImageConverterTest { context, IMAGE_SIZE_ORIGINAL ) - val image = ImageFileUtils.getBitmap(testImagePath, BitmapFactory.Options()) - assertEquals(3000, image!!.width) - assertEquals(3000, image.height) + + val image = ImageFileUtils.getBitmap(testImagePath, BitmapFactory.Options())!! + + assertThat(3000, equalTo(image.width)) + assertThat(3000, equalTo(image.height)) } @Test fun scaleImageDownFormLevelOnly12() { - generalSettings.save("image_size", "original_image_size") saveTestBitmap(3000, 3000) ImageConverter.execute( testImagePath, @@ -260,64 +238,70 @@ class ImageConverterTest { context, IMAGE_SIZE_ORIGINAL ) - val image = ImageFileUtils.getBitmap(testImagePath, BitmapFactory.Options()) - assertEquals(3000, image!!.width) - assertEquals(3000, image.height) + + val image = ImageFileUtils.getBitmap(testImagePath, BitmapFactory.Options())!! + + assertThat(3000, equalTo(image.width)) + assertThat(3000, equalTo(image.height)) } @Test fun scaleImageDownSettingsLevelOnly1() { - generalSettings.save("image_size", IMAGE_SIZE_VERY_SMALL) saveTestBitmap(3000, 3000) ImageConverter.execute(testImagePath, getTestImageWidget(), context, IMAGE_SIZE_VERY_SMALL) - val image = ImageFileUtils.getBitmap(testImagePath, BitmapFactory.Options()) - assertEquals(640, image!!.width) - assertEquals(640, image.height) + + val image = ImageFileUtils.getBitmap(testImagePath, BitmapFactory.Options())!! + + assertThat(640, equalTo(image.width)) + assertThat(640, equalTo(image.height)) } @Test fun scaleImageDownSettingsLevelOnly2() { - generalSettings.save("image_size", IMAGE_SIZE_SMALL) saveTestBitmap(3000, 3000) ImageConverter.execute(testImagePath, getTestImageWidget(), context, IMAGE_SIZE_SMALL) - val image = ImageFileUtils.getBitmap(testImagePath, BitmapFactory.Options()) - assertEquals(1024, image!!.width) - assertEquals(1024, image.height) + + val image = ImageFileUtils.getBitmap(testImagePath, BitmapFactory.Options())!! + + assertThat(1024, equalTo(image.width)) + assertThat(1024, equalTo(image.height)) } @Test fun scaleImageDownSettingsLevelOnly3() { - generalSettings.save("image_size", IMAGE_SIZE_MEDIUM) saveTestBitmap(3000, 3000) ImageConverter.execute(testImagePath, getTestImageWidget(), context, IMAGE_SIZE_MEDIUM) - val image = ImageFileUtils.getBitmap(testImagePath, BitmapFactory.Options()) - assertEquals(2048, image!!.width) - assertEquals(2048, image.height) + + val image = ImageFileUtils.getBitmap(testImagePath, BitmapFactory.Options())!! + + assertThat(2048, equalTo(image.width)) + assertThat(2048, equalTo(image.height)) } @Test fun scaleImageDownSettingsLevelOnly4() { - generalSettings.save("image_size", IMAGE_SIZE_LARGE) saveTestBitmap(3000, 3000) ImageConverter.execute(testImagePath, getTestImageWidget(), context, IMAGE_SIZE_LARGE) - val image = ImageFileUtils.getBitmap(testImagePath, BitmapFactory.Options()) - assertEquals(3000, image!!.width) - assertEquals(3000, image.height) + + val image = ImageFileUtils.getBitmap(testImagePath, BitmapFactory.Options())!! + + assertThat(3000, equalTo(image.width)) + assertThat(3000, equalTo(image.height)) } @Test fun scaleImageDownSettingsLevelOnly5() { - generalSettings.save("image_size", IMAGE_SIZE_LARGE) saveTestBitmap(4000, 4000) ImageConverter.execute(testImagePath, getTestImageWidget(), context, IMAGE_SIZE_LARGE) - val image = ImageFileUtils.getBitmap(testImagePath, BitmapFactory.Options()) - assertEquals(3072, image!!.width) - assertEquals(3072, image.height) + + val image = ImageFileUtils.getBitmap(testImagePath, BitmapFactory.Options())!! + + assertThat(3072, equalTo(image.width)) + assertThat(3072, equalTo(image.height)) } @Test fun scaleImageDownFormAndSettingsLevel1() { - generalSettings.save("image_size", IMAGE_SIZE_SMALL) saveTestBitmap(4000, 4000) ImageConverter.execute( testImagePath, @@ -325,14 +309,15 @@ class ImageConverterTest { context, IMAGE_SIZE_SMALL ) - val image = ImageFileUtils.getBitmap(testImagePath, BitmapFactory.Options()) - assertEquals(2000, image!!.width) - assertEquals(2000, image.height) + + val image = ImageFileUtils.getBitmap(testImagePath, BitmapFactory.Options())!! + + assertThat(2000, equalTo(image.width)) + assertThat(2000, equalTo(image.height)) } @Test fun scaleImageDownFormAndSettingsLevel2() { - generalSettings.save("image_size", "small") saveTestBitmap(4000, 4000) ImageConverter.execute( testImagePath, @@ -340,85 +325,83 @@ class ImageConverterTest { context, IMAGE_SIZE_SMALL ) - val image = ImageFileUtils.getBitmap(testImagePath, BitmapFactory.Options()) - assertEquals(650, image!!.width) - assertEquals(650, image.height) + + val image = ImageFileUtils.getBitmap(testImagePath, BitmapFactory.Options())!! + + assertThat(650, equalTo(image.width)) + assertThat(650, equalTo(image.height)) } @Test fun keepExifAfterScaling() { - val attributes: MutableMap = HashMap() - attributes[ExifInterface.TAG_ARTIST] = ExifInterface.TAG_ARTIST - attributes[ExifInterface.TAG_DATETIME] = ExifInterface.TAG_DATETIME - attributes[ExifInterface.TAG_DATETIME_ORIGINAL] = ExifInterface.TAG_DATETIME_ORIGINAL - attributes[ExifInterface.TAG_DATETIME_DIGITIZED] = ExifInterface.TAG_DATETIME_DIGITIZED - attributes[ExifInterface.TAG_GPS_ALTITUDE] = dec2DMS(-17.0) - attributes[ExifInterface.TAG_GPS_ALTITUDE_REF] = ExifInterface.TAG_GPS_ALTITUDE_REF - attributes[ExifInterface.TAG_GPS_DATESTAMP] = ExifInterface.TAG_GPS_DATESTAMP - attributes[ExifInterface.TAG_GPS_LATITUDE] = dec2DMS(25.165173) - attributes[ExifInterface.TAG_GPS_LATITUDE_REF] = ExifInterface.TAG_GPS_LATITUDE_REF - attributes[ExifInterface.TAG_GPS_LONGITUDE] = dec2DMS(23.988174) - attributes[ExifInterface.TAG_GPS_LONGITUDE_REF] = ExifInterface.TAG_GPS_LONGITUDE_REF - attributes[ExifInterface.TAG_GPS_PROCESSING_METHOD] = ExifInterface.TAG_GPS_PROCESSING_METHOD - attributes[ExifInterface.TAG_MAKE] = ExifInterface.TAG_MAKE - attributes[ExifInterface.TAG_MODEL] = ExifInterface.TAG_MODEL - attributes[ExifInterface.TAG_SUBSEC_TIME] = ExifInterface.TAG_SUBSEC_TIME + val attributes = mutableMapOf( + ExifInterface.TAG_DATETIME to "2014:01:23 14:57:18", + ExifInterface.TAG_DATETIME_ORIGINAL to "2014:01:23 14:57:18", + ExifInterface.TAG_DATETIME_DIGITIZED to "2014:01:23 14:57:18", + ExifInterface.TAG_OFFSET_TIME to "+1:00", + ExifInterface.TAG_OFFSET_TIME_ORIGINAL to "+1:00", + ExifInterface.TAG_OFFSET_TIME_DIGITIZED to "+1:00", + ExifInterface.TAG_SUBSEC_TIME to "First photo", + ExifInterface.TAG_SUBSEC_TIME_ORIGINAL to "0", + ExifInterface.TAG_SUBSEC_TIME_DIGITIZED to "0", + ExifInterface.TAG_IMAGE_DESCRIPTION to "Photo from Poland", + ExifInterface.TAG_MAKE to "OLYMPUS IMAGING CORP", + ExifInterface.TAG_MODEL to "STYLUS1", + ExifInterface.TAG_SOFTWARE to "Version 1.0", + ExifInterface.TAG_ARTIST to "Grzegorz", + ExifInterface.TAG_COPYRIGHT to "G", + ExifInterface.TAG_MAKER_NOTE to "OLYMPUS", + ExifInterface.TAG_USER_COMMENT to "First photo", + ExifInterface.TAG_IMAGE_UNIQUE_ID to "123456789", + ExifInterface.TAG_CAMERA_OWNER_NAME to "John", + ExifInterface.TAG_BODY_SERIAL_NUMBER to "987654321", + ExifInterface.TAG_GPS_ALTITUDE to "41/1", + ExifInterface.TAG_GPS_ALTITUDE_REF to "0", + ExifInterface.TAG_GPS_DATESTAMP to "2014:01:23", + ExifInterface.TAG_GPS_TIMESTAMP to "14:57:18", + ExifInterface.TAG_GPS_LATITUDE to "50/1,49/1,8592/1000", + ExifInterface.TAG_GPS_LATITUDE_REF to "N", + ExifInterface.TAG_GPS_LONGITUDE to "0/1,8/1,12450/1000", + ExifInterface.TAG_GPS_LONGITUDE_REF to "W", + ExifInterface.TAG_GPS_SATELLITES to "8", + ExifInterface.TAG_GPS_STATUS to "A", + ExifInterface.TAG_ORIENTATION to "1" + ) saveTestBitmap(3000, 4000, attributes) - ImageConverter.execute(testImagePath, getTestImageWidget(), context, IMAGE_SIZE_ORIGINAL) - val exifData = testImageExif - assertNotNull(exifData) + ImageConverter.execute(testImagePath, getTestImageWidget(), context, IMAGE_SIZE_VERY_SMALL) + val exifData = ExifInterface(testImagePath) for (attributeName in attributes.keys) { - when (attributeName) { - ExifInterface.TAG_GPS_LATITUDE -> assertThat( - exifData!!.getAttribute(attributeName), `is`("25/1,9/1,54622/1000") - ) - ExifInterface.TAG_GPS_LONGITUDE -> assertThat( - exifData!!.getAttribute(attributeName), `is`("23/1,59/1,17426/1000") - ) - ExifInterface.TAG_GPS_ALTITUDE -> assertThat( - exifData!!.getAttribute(attributeName), `is`("17/1,0/1,0/1000") - ) - else -> assertThat(exifData!!.getAttribute(attributeName), `is`(attributeName)) - } + assertThat(exifData.getAttribute(attributeName), equalTo(attributes[attributeName])) } } @Test fun verifyNoRotationAppliedForExifRotation() { - val attributes: MutableMap = HashMap() - attributes[ExifInterface.TAG_ORIENTATION] = ExifInterface.ORIENTATION_ROTATE_90.toString() + val attributes = mapOf(ExifInterface.TAG_ORIENTATION to ExifInterface.ORIENTATION_ROTATE_90.toString()) saveTestBitmap(3000, 4000, attributes) ImageConverter.execute(testImagePath, getTestImageWidget(), context, IMAGE_SIZE_ORIGINAL) - val image = ImageFileUtils.getBitmap(testImagePath, BitmapFactory.Options()) - assertEquals(3000, image!!.width) - assertEquals(4000, image.height) - } - // https://stackoverflow.com/a/55252228/5479029 - private fun dec2DMS(coord: Double): String { - var coord = coord - coord = if (coord > 0) coord else -coord - var out = "${coord.toInt()}/1," - coord = coord % 1 * 60 - out = "${out + coord.toInt()}/1," - coord = coord % 1 * 60000 - out = "${out + coord.toInt()}/1000" - return out + val image = ImageFileUtils.getBitmap(testImagePath, BitmapFactory.Options())!! + + assertThat(3000, equalTo(image.width)) + assertThat(4000, equalTo(image.height)) } private fun saveTestBitmap( width: Int, height: Int, - attributes: Map = HashMap() + attributes: Map = emptyMap() ) { + testImagePath = File.createTempFile("test", ".jpg").absolutePath + val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565) ImageFileUtils.saveBitmapToFile(bitmap, testImagePath) try { val exifInterface = ExifInterface(testImagePath) - for (attributeName in attributes.keys) { - exifInterface.setAttribute(attributeName, attributes[attributeName]) + for ((key, value) in attributes) { + exifInterface.setAttribute(key, value) } exifInterface.saveAttributes() } catch (e: IOException) { @@ -426,16 +409,6 @@ class ImageConverterTest { } } - private val testImageExif: ExifInterface? - get() { - try { - return ExifInterface(testImagePath) - } catch (e: Exception) { - Timber.w(e) - } - return null - } - private fun getTestImageWidget(namespace: String, name: String, value: String): ImageWidget { val bindAttributes: MutableList = mutableListOf() bindAttributes.add(TreeElement.constructAttributeElement(namespace, name, value)) From 4c0e1b86f7c0c958d71289ca251ed42d966961e1 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Fri, 24 Feb 2023 12:43:51 +0100 Subject: [PATCH 4/6] Separated pure image compression from checking if it should happen at all --- ...onverterTest.kt => ImageCompressorTest.kt} | 0 .../injection/config/AppDependencyModule.java | 8 +++ .../android/tasks/MediaLoadingTask.java | 7 ++- .../utilities/ImageCompressionController.kt | 48 +++++++++++++++ .../{ImageConverter.kt => ImageCompressor.kt} | 59 +++---------------- .../ImageCompressionControllerTest.kt | 5 ++ 6 files changed, 73 insertions(+), 54 deletions(-) rename collect_app/src/androidTest/java/org/odk/collect/android/instrumented/utilities/{ImageConverterTest.kt => ImageCompressorTest.kt} (100%) create mode 100644 collect_app/src/main/java/org/odk/collect/android/utilities/ImageCompressionController.kt rename collect_app/src/main/java/org/odk/collect/android/utilities/{ImageConverter.kt => ImageCompressor.kt} (70%) create mode 100644 collect_app/src/test/java/org/odk/collect/android/utilities/ImageCompressionControllerTest.kt diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/instrumented/utilities/ImageConverterTest.kt b/collect_app/src/androidTest/java/org/odk/collect/android/instrumented/utilities/ImageCompressorTest.kt similarity index 100% rename from collect_app/src/androidTest/java/org/odk/collect/android/instrumented/utilities/ImageConverterTest.kt rename to collect_app/src/androidTest/java/org/odk/collect/android/instrumented/utilities/ImageCompressorTest.kt diff --git a/collect_app/src/main/java/org/odk/collect/android/injection/config/AppDependencyModule.java b/collect_app/src/main/java/org/odk/collect/android/injection/config/AppDependencyModule.java index 5ef3827d752..f33e5586e45 100644 --- a/collect_app/src/main/java/org/odk/collect/android/injection/config/AppDependencyModule.java +++ b/collect_app/src/main/java/org/odk/collect/android/injection/config/AppDependencyModule.java @@ -111,6 +111,8 @@ import org.odk.collect.android.utilities.FileProvider; import org.odk.collect.android.utilities.FormsDirDiskFormsSynchronizer; import org.odk.collect.android.utilities.FormsRepositoryProvider; +import org.odk.collect.android.utilities.ImageCompressor; +import org.odk.collect.android.utilities.ImageCompressionController; import org.odk.collect.android.utilities.InstancesRepositoryProvider; import org.odk.collect.android.utilities.MediaUtils; import org.odk.collect.android.utilities.ProjectResetter; @@ -684,4 +686,10 @@ public ProjectDependencyProviderFactory providesProjectDependencyProviderFactory public BlankFormListViewModel.Factory providesBlankFormListViewModel(FormsRepositoryProvider formsRepositoryProvider, InstancesRepositoryProvider instancesRepositoryProvider, Application application, SyncStatusAppState syncStatusAppState, FormsUpdater formsUpdater, Scheduler scheduler, SettingsProvider settingsProvider, ChangeLockProvider changeLockProvider, CurrentProjectProvider currentProjectProvider) { return new BlankFormListViewModel.Factory(formsRepositoryProvider.get(), instancesRepositoryProvider.get(), application, syncStatusAppState, formsUpdater, scheduler, settingsProvider.getUnprotectedSettings(), changeLockProvider, new FormsDirDiskFormsSynchronizer(), currentProjectProvider.getCurrentProject().getUuid()); } + + @Provides + @Singleton + public ImageCompressionController providesImageCompressorManager() { + return new ImageCompressionController(ImageCompressor.INSTANCE); + } } diff --git a/collect_app/src/main/java/org/odk/collect/android/tasks/MediaLoadingTask.java b/collect_app/src/main/java/org/odk/collect/android/tasks/MediaLoadingTask.java index 96b578aea56..03cc1357f71 100644 --- a/collect_app/src/main/java/org/odk/collect/android/tasks/MediaLoadingTask.java +++ b/collect_app/src/main/java/org/odk/collect/android/tasks/MediaLoadingTask.java @@ -10,7 +10,7 @@ import org.odk.collect.android.injection.DaggerUtils; import org.odk.collect.android.utilities.ContentUriHelper; import org.odk.collect.android.utilities.FileUtils; -import org.odk.collect.android.utilities.ImageConverter; +import org.odk.collect.android.utilities.ImageCompressionController; import org.odk.collect.android.widgets.BaseImageWidget; import org.odk.collect.android.widgets.QuestionWidget; import org.odk.collect.androidshared.ui.DialogFragmentUtils; @@ -27,6 +27,9 @@ public class MediaLoadingTask extends AsyncTask { @Inject SettingsProvider settingsProvider; + @Inject + ImageCompressionController imageCompressionController; + private WeakReference formEntryActivity; public MediaLoadingTask(FormEntryActivity formEntryActivity, File instanceFile) { @@ -51,7 +54,7 @@ protected File doInBackground(Uri... uris) { // apply image conversion if the widget is an image widget if (questionWidget instanceof BaseImageWidget) { String imageSizeMode = settingsProvider.getUnprotectedSettings().getString(KEY_IMAGE_SIZE); - ImageConverter.execute(newFile.getPath(), questionWidget, formEntryActivity.get(), imageSizeMode); + imageCompressionController.execute(newFile.getPath(), questionWidget, formEntryActivity.get(), imageSizeMode); } return newFile; } diff --git a/collect_app/src/main/java/org/odk/collect/android/utilities/ImageCompressionController.kt b/collect_app/src/main/java/org/odk/collect/android/utilities/ImageCompressionController.kt new file mode 100644 index 00000000000..611e5317424 --- /dev/null +++ b/collect_app/src/main/java/org/odk/collect/android/utilities/ImageCompressionController.kt @@ -0,0 +1,48 @@ +package org.odk.collect.android.utilities + +import android.content.Context +import org.odk.collect.android.R +import org.odk.collect.android.widgets.QuestionWidget +import timber.log.Timber + +class ImageCompressionController(private val imageCompressor: ImageCompressor) { + fun execute( + imagePath: String, + questionWidget: QuestionWidget, + context: Context, + imageSizeMode: String + ) { + var maxPixels: Int? + maxPixels = getMaxPixelsFromFormIfDefined(questionWidget) + if (maxPixels == null) { + maxPixels = getMaxPixelsFromSettings(context, imageSizeMode) + } + if (maxPixels != null && maxPixels > 0) { + imageCompressor.execute(imagePath, maxPixels) + } + } + + private fun getMaxPixelsFromFormIfDefined(questionWidget: QuestionWidget): Int? { + for (bindAttribute in questionWidget.formEntryPrompt.bindAttributes) { + if ("max-pixels" == bindAttribute.name && ApplicationConstants.Namespaces.XML_OPENROSA_NAMESPACE == bindAttribute.namespace) { + try { + return bindAttribute.attributeValue.toInt() + } catch (e: NumberFormatException) { + Timber.i(e) + } + } + } + return null + } + + private fun getMaxPixelsFromSettings(context: Context, imageSizeMode: String): Int? { + val imageEntryValues = context.resources.getStringArray(R.array.image_size_entry_values) + return when (imageSizeMode) { + imageEntryValues[1] -> 640 + imageEntryValues[2] -> 1024 + imageEntryValues[3] -> 2048 + imageEntryValues[4] -> 3072 + else -> null + } + } +} diff --git a/collect_app/src/main/java/org/odk/collect/android/utilities/ImageConverter.kt b/collect_app/src/main/java/org/odk/collect/android/utilities/ImageCompressor.kt similarity index 70% rename from collect_app/src/main/java/org/odk/collect/android/utilities/ImageConverter.kt rename to collect_app/src/main/java/org/odk/collect/android/utilities/ImageCompressor.kt index 458192b654b..4d9a52fbede 100644 --- a/collect_app/src/main/java/org/odk/collect/android/utilities/ImageConverter.kt +++ b/collect_app/src/main/java/org/odk/collect/android/utilities/ImageCompressor.kt @@ -15,76 +15,31 @@ */ package org.odk.collect.android.utilities -import android.content.Context import android.graphics.Bitmap import android.graphics.BitmapFactory import androidx.exifinterface.media.ExifInterface -import org.odk.collect.android.R -import org.odk.collect.android.widgets.QuestionWidget import timber.log.Timber -object ImageConverter { +object ImageCompressor { /** * Before proceed with scaling or rotating, make sure existing exif information is stored/restored. * @author Khuong Ninh (khuong.ninh@it-development.com) */ - @JvmStatic - fun execute( - imagePath: String, - questionWidget: QuestionWidget, - context: Context, - imageSizeMode: String - ) { + fun execute(imagePath: String, maxPixels: Int) { backupExifData(imagePath) - scaleDownImageIfNeeded(imagePath, questionWidget, context, imageSizeMode) + scaleDownImage(imagePath, maxPixels) restoreExifData(imagePath) } - private fun scaleDownImageIfNeeded( - imagePath: String, - questionWidget: QuestionWidget, - context: Context, - imageSizeMode: String - ) { - var maxPixels: Int? - maxPixels = getMaxPixelsFromFormIfDefined(questionWidget) - if (maxPixels == null) { - maxPixels = getMaxPixelsFromSettings(context, imageSizeMode) - } - if (maxPixels != null && maxPixels > 0) { - scaleDownImage(imagePath, maxPixels) - } - } - - private fun getMaxPixelsFromFormIfDefined(questionWidget: QuestionWidget): Int? { - for (attrs in questionWidget.formEntryPrompt.bindAttributes) { - if ("max-pixels" == attrs.name && ApplicationConstants.Namespaces.XML_OPENROSA_NAMESPACE == attrs.namespace) { - try { - return attrs.attributeValue.toInt() - } catch (e: NumberFormatException) { - Timber.i(e) - } - } - } - return null - } - - private fun getMaxPixelsFromSettings(context: Context, imageSizeMode: String): Int? { - val imageEntryValues = context.resources.getStringArray(R.array.image_size_entry_values) - return when (imageSizeMode) { - imageEntryValues[1] -> 640 - imageEntryValues[2] -> 1024 - imageEntryValues[3] -> 2048 - imageEntryValues[4] -> 3072 - else -> null - } - } - /** * This method is used to reduce an original picture size. * maxPixels refers to the max pixels of the long edge, the short edge is scaled proportionately. */ private fun scaleDownImage(imagePath: String, maxPixels: Int) { + if (maxPixels <= 0) { + return + } + var image = ImageFileUtils.getBitmap(imagePath, BitmapFactory.Options()) if (image != null) { val originalWidth = image.width.toDouble() diff --git a/collect_app/src/test/java/org/odk/collect/android/utilities/ImageCompressionControllerTest.kt b/collect_app/src/test/java/org/odk/collect/android/utilities/ImageCompressionControllerTest.kt new file mode 100644 index 00000000000..b69617016d5 --- /dev/null +++ b/collect_app/src/test/java/org/odk/collect/android/utilities/ImageCompressionControllerTest.kt @@ -0,0 +1,5 @@ +package org.odk.collect.android.utilities + +import org.junit.jupiter.api.Assertions.* + +internal class ImageCompressionControllerTest \ No newline at end of file From 9695d0b7b450f5bb17df24bde61380642641d42e Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Fri, 24 Feb 2023 12:44:09 +0100 Subject: [PATCH 5/6] Added tests --- .../utilities/ImageCompressorTest.kt | 335 +++--------------- .../ImageCompressionControllerTest.kt | 170 ++++++++- 2 files changed, 220 insertions(+), 285 deletions(-) diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/instrumented/utilities/ImageCompressorTest.kt b/collect_app/src/androidTest/java/org/odk/collect/android/instrumented/utilities/ImageCompressorTest.kt index 46e7652a33e..d215bd57461 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/instrumented/utilities/ImageCompressorTest.kt +++ b/collect_app/src/androidTest/java/org/odk/collect/android/instrumented/utilities/ImageCompressorTest.kt @@ -15,229 +15,71 @@ */ package org.odk.collect.android.instrumented.utilities -import android.content.Context import android.graphics.Bitmap import android.graphics.BitmapFactory import androidx.exifinterface.media.ExifInterface -import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers.equalTo -import org.javarosa.core.model.instance.TreeElement -import org.javarosa.form.api.FormEntryPrompt import org.junit.Test import org.junit.runner.RunWith -import org.mockito.Mockito.`when` -import org.mockito.Mockito.mock -import org.odk.collect.android.utilities.ApplicationConstants.Namespaces -import org.odk.collect.android.utilities.ImageConverter +import org.odk.collect.android.utilities.ImageCompressor import org.odk.collect.android.utilities.ImageFileUtils -import org.odk.collect.android.widgets.ImageWidget -import timber.log.Timber import java.io.File -import java.io.IOException @RunWith(AndroidJUnit4::class) -class ImageConverterTest { +class ImageCompressorTest { private lateinit var testImagePath: String - private val context = ApplicationProvider.getApplicationContext() + private val imageCompressor = ImageCompressor @Test - fun executeConversionWithoutAnySettings() { - saveTestBitmap(3000, 3000) - ImageConverter.execute(testImagePath, getTestImageWidget(), context, IMAGE_SIZE_ORIGINAL) + fun imageShouldNotBeChangedIfMaxPixelsIsZero() { + saveTestBitmap(3000, 2000) + imageCompressor.execute(testImagePath, 0) val image = ImageFileUtils.getBitmap(testImagePath, BitmapFactory.Options())!! assertThat(3000, equalTo(image.width)) - assertThat(3000, equalTo(image.height)) - } - - @Test - fun scaleImageDownFormLevelOnly1() { - saveTestBitmap(4000, 3000) - ImageConverter.execute( - testImagePath, - getTestImageWidget(Namespaces.XML_OPENROSA_NAMESPACE, "max-pixels", "2000"), - context, - IMAGE_SIZE_ORIGINAL - ) - - val image = ImageFileUtils.getBitmap(testImagePath, BitmapFactory.Options())!! - - assertThat(2000, equalTo(image.width)) - assertThat(1500, equalTo(image.height)) - } - - @Test - fun scaleImageDownFormLevelOnly2() { - saveTestBitmap(3000, 4000) - ImageConverter.execute( - testImagePath, - getTestImageWidget(Namespaces.XML_OPENROSA_NAMESPACE, "max-pixels", "2000"), - context, - IMAGE_SIZE_ORIGINAL - ) - - val image = ImageFileUtils.getBitmap(testImagePath, BitmapFactory.Options())!! - - assertThat(1500, equalTo(image.width)) assertThat(2000, equalTo(image.height)) } @Test - fun scaleImageDownFormLevelOnly3() { - saveTestBitmap(3000, 3000) - ImageConverter.execute( - testImagePath, - getTestImageWidget(Namespaces.XML_OPENROSA_NAMESPACE, "max-pixels", "2000"), - context, - IMAGE_SIZE_ORIGINAL - ) - - val image = ImageFileUtils.getBitmap(testImagePath, BitmapFactory.Options())!! - - assertThat(2000, equalTo(image.width)) - assertThat(2000, equalTo(image.height)) - } - - @Test - fun scaleImageDownFormLevelOnly4() { - saveTestBitmap(3000, 3000) - ImageConverter.execute( - testImagePath, - getTestImageWidget(Namespaces.XML_OPENROSA_NAMESPACE, "max-pixels", "3000"), - context, - IMAGE_SIZE_ORIGINAL - ) - - val image = ImageFileUtils.getBitmap(testImagePath, BitmapFactory.Options())!! - - assertThat(3000, equalTo(image.width)) - assertThat(3000, equalTo(image.height)) - } - - @Test - fun scaleImageDownFormLevelOnly5() { - saveTestBitmap(3000, 3000) - ImageConverter.execute( - testImagePath, - getTestImageWidget(Namespaces.XML_OPENROSA_NAMESPACE, "max-pixels", "4000"), - context, - IMAGE_SIZE_ORIGINAL - ) - - val image = ImageFileUtils.getBitmap(testImagePath, BitmapFactory.Options())!! - - assertThat(3000, equalTo(image.width)) - assertThat(3000, equalTo(image.height)) - } - - @Test - fun scaleImageDownFormLevelOnly6() { - saveTestBitmap(3000, 3000) - ImageConverter.execute( - testImagePath, - getTestImageWidget(Namespaces.XML_OPENROSA_NAMESPACE, "max-pixels", "2998"), - context, - IMAGE_SIZE_ORIGINAL - ) - - val image = ImageFileUtils.getBitmap(testImagePath, BitmapFactory.Options())!! - - assertThat(2998, equalTo(image.width)) - assertThat(2998, equalTo(image.height)) - } - - @Test - fun scaleImageDownFormLevelOnly7() { - saveTestBitmap(3000, 3000) - ImageConverter.execute( - testImagePath, - getTestImageWidget(Namespaces.XML_OPENROSA_NAMESPACE, "max-pixels", ""), - context, - IMAGE_SIZE_ORIGINAL - ) - - val image = ImageFileUtils.getBitmap(testImagePath, BitmapFactory.Options())!! - - assertThat(3000, equalTo(image.width)) - assertThat(3000, equalTo(image.height)) - } - - @Test - fun scaleImageDownFormLevelOnly8() { - saveTestBitmap(3000, 3000) - ImageConverter.execute( - testImagePath, - getTestImageWidget("", "max-pixels", "2000"), - context, - IMAGE_SIZE_ORIGINAL - ) + fun imageShouldNotBeChangedIfMaxPixelsIsSmallerThanZero() { + saveTestBitmap(3000, 2000) + imageCompressor.execute(testImagePath, -10) val image = ImageFileUtils.getBitmap(testImagePath, BitmapFactory.Options())!! assertThat(3000, equalTo(image.width)) - assertThat(3000, equalTo(image.height)) + assertThat(2000, equalTo(image.height)) } @Test - fun scaleImageDownFormLevelOnly9() { - saveTestBitmap(3000, 3000) - ImageConverter.execute( - testImagePath, - getTestImageWidget(Namespaces.XML_OPENROSA_NAMESPACE, "max-pixel", "2000"), - context, - IMAGE_SIZE_ORIGINAL - ) + fun imageShouldNotBeChangedIfMaxPixelsIsNotSmallerThanTheEdgeWhenWidthIsBiggerThanHeight() { + saveTestBitmap(3000, 2000) + imageCompressor.execute(testImagePath, 3000) val image = ImageFileUtils.getBitmap(testImagePath, BitmapFactory.Options())!! assertThat(3000, equalTo(image.width)) - assertThat(3000, equalTo(image.height)) + assertThat(2000, equalTo(image.height)) } @Test - fun scaleImageDownFormLevelOnly10() { - saveTestBitmap(3000, 3000) - ImageConverter.execute( - testImagePath, - getTestImageWidget(Namespaces.XML_OPENROSA_NAMESPACE, "max-pixels", "2000.5"), - context, - IMAGE_SIZE_ORIGINAL - ) + fun imageShouldNotBeChangedIfMaxPixelsIsNotSmallerThanTheLongEdgeWhenWidthIsSmallerThanHeight() { + saveTestBitmap(2000, 3000) + imageCompressor.execute(testImagePath, 4000) val image = ImageFileUtils.getBitmap(testImagePath, BitmapFactory.Options())!! - assertThat(3000, equalTo(image.width)) - assertThat(3000, equalTo(image.height)) - } - - @Test - fun scaleImageDownFormLevelOnly11() { - saveTestBitmap(3000, 3000) - ImageConverter.execute( - testImagePath, - getTestImageWidget(Namespaces.XML_OPENROSA_NAMESPACE, "max-pixels", "0"), - context, - IMAGE_SIZE_ORIGINAL - ) - - val image = ImageFileUtils.getBitmap(testImagePath, BitmapFactory.Options())!! - - assertThat(3000, equalTo(image.width)) + assertThat(2000, equalTo(image.width)) assertThat(3000, equalTo(image.height)) } @Test - fun scaleImageDownFormLevelOnly12() { + fun imageShouldNotBeChangedIfMaxPixelsIsNotSmallerThanTheLongEdgeWhenWidthEqualsHeight() { saveTestBitmap(3000, 3000) - ImageConverter.execute( - testImagePath, - getTestImageWidget(Namespaces.XML_OPENROSA_NAMESPACE, "max-pixels", "-2000"), - context, - IMAGE_SIZE_ORIGINAL - ) + imageCompressor.execute(testImagePath, 3000) val image = ImageFileUtils.getBitmap(testImagePath, BitmapFactory.Options())!! @@ -246,69 +88,31 @@ class ImageConverterTest { } @Test - fun scaleImageDownSettingsLevelOnly1() { - saveTestBitmap(3000, 3000) - ImageConverter.execute(testImagePath, getTestImageWidget(), context, IMAGE_SIZE_VERY_SMALL) - - val image = ImageFileUtils.getBitmap(testImagePath, BitmapFactory.Options())!! - - assertThat(640, equalTo(image.width)) - assertThat(640, equalTo(image.height)) - } - - @Test - fun scaleImageDownSettingsLevelOnly2() { - saveTestBitmap(3000, 3000) - ImageConverter.execute(testImagePath, getTestImageWidget(), context, IMAGE_SIZE_SMALL) + fun imageShouldBeCompressedIfMaxPixelsIsSmallerThanTheLongEdgeWhenWidthIsBiggerThanHeight() { + saveTestBitmap(4000, 3000) + imageCompressor.execute(testImagePath, 2000) val image = ImageFileUtils.getBitmap(testImagePath, BitmapFactory.Options())!! - assertThat(1024, equalTo(image.width)) - assertThat(1024, equalTo(image.height)) + assertThat(2000, equalTo(image.width)) + assertThat(1500, equalTo(image.height)) } @Test - fun scaleImageDownSettingsLevelOnly3() { - saveTestBitmap(3000, 3000) - ImageConverter.execute(testImagePath, getTestImageWidget(), context, IMAGE_SIZE_MEDIUM) + fun imageShouldBeCompressedIfMaxPixelsIsSmallerThanTheLongEdgeWhenWidthIsSmallerThanHeight() { + saveTestBitmap(3000, 4000) + imageCompressor.execute(testImagePath, 2000) val image = ImageFileUtils.getBitmap(testImagePath, BitmapFactory.Options())!! - assertThat(2048, equalTo(image.width)) - assertThat(2048, equalTo(image.height)) + assertThat(1500, equalTo(image.width)) + assertThat(2000, equalTo(image.height)) } @Test - fun scaleImageDownSettingsLevelOnly4() { + fun imageShouldBeCompressedIfMaxPixelsIsSmallerThanTheLongEdgeWhenWidthEqualsHeight() { saveTestBitmap(3000, 3000) - ImageConverter.execute(testImagePath, getTestImageWidget(), context, IMAGE_SIZE_LARGE) - - val image = ImageFileUtils.getBitmap(testImagePath, BitmapFactory.Options())!! - - assertThat(3000, equalTo(image.width)) - assertThat(3000, equalTo(image.height)) - } - - @Test - fun scaleImageDownSettingsLevelOnly5() { - saveTestBitmap(4000, 4000) - ImageConverter.execute(testImagePath, getTestImageWidget(), context, IMAGE_SIZE_LARGE) - - val image = ImageFileUtils.getBitmap(testImagePath, BitmapFactory.Options())!! - - assertThat(3072, equalTo(image.width)) - assertThat(3072, equalTo(image.height)) - } - - @Test - fun scaleImageDownFormAndSettingsLevel1() { - saveTestBitmap(4000, 4000) - ImageConverter.execute( - testImagePath, - getTestImageWidget(Namespaces.XML_OPENROSA_NAMESPACE, "max-pixels", "2000"), - context, - IMAGE_SIZE_SMALL - ) + imageCompressor.execute(testImagePath, 2000) val image = ImageFileUtils.getBitmap(testImagePath, BitmapFactory.Options())!! @@ -316,25 +120,10 @@ class ImageConverterTest { assertThat(2000, equalTo(image.height)) } - @Test - fun scaleImageDownFormAndSettingsLevel2() { - saveTestBitmap(4000, 4000) - ImageConverter.execute( - testImagePath, - getTestImageWidget(Namespaces.XML_OPENROSA_NAMESPACE, "max-pixels", "650"), - context, - IMAGE_SIZE_SMALL - ) - - val image = ImageFileUtils.getBitmap(testImagePath, BitmapFactory.Options())!! - - assertThat(650, equalTo(image.width)) - assertThat(650, equalTo(image.height)) - } - @Test fun keepExifAfterScaling() { val attributes = mutableMapOf( + // supported exif tags ExifInterface.TAG_DATETIME to "2014:01:23 14:57:18", ExifInterface.TAG_DATETIME_ORIGINAL to "2014:01:23 14:57:18", ExifInterface.TAG_DATETIME_DIGITIZED to "2014:01:23 14:57:18", @@ -365,15 +154,25 @@ class ImageConverterTest { ExifInterface.TAG_GPS_LONGITUDE_REF to "W", ExifInterface.TAG_GPS_SATELLITES to "8", ExifInterface.TAG_GPS_STATUS to "A", - ExifInterface.TAG_ORIENTATION to "1" + ExifInterface.TAG_ORIENTATION to "1", + + // unsupported exif tags + ExifInterface.TAG_THUMBNAIL_IMAGE_LENGTH to "5", + ExifInterface.TAG_DNG_VERSION to "100", ) saveTestBitmap(3000, 4000, attributes) - ImageConverter.execute(testImagePath, getTestImageWidget(), context, IMAGE_SIZE_VERY_SMALL) + imageCompressor.execute(testImagePath, 2000) val exifData = ExifInterface(testImagePath) for (attributeName in attributes.keys) { - assertThat(exifData.getAttribute(attributeName), equalTo(attributes[attributeName])) + if (attributeName == ExifInterface.TAG_THUMBNAIL_IMAGE_LENGTH || + attributeName == ExifInterface.TAG_DNG_VERSION + ) { + assertThat(exifData.getAttribute(attributeName), equalTo(null)) + } else { + assertThat(exifData.getAttribute(attributeName), equalTo(attributes[attributeName])) + } } } @@ -381,7 +180,7 @@ class ImageConverterTest { fun verifyNoRotationAppliedForExifRotation() { val attributes = mapOf(ExifInterface.TAG_ORIENTATION to ExifInterface.ORIENTATION_ROTATE_90.toString()) saveTestBitmap(3000, 4000, attributes) - ImageConverter.execute(testImagePath, getTestImageWidget(), context, IMAGE_SIZE_ORIGINAL) + imageCompressor.execute(testImagePath, 4000) val image = ImageFileUtils.getBitmap(testImagePath, BitmapFactory.Options())!! @@ -389,45 +188,15 @@ class ImageConverterTest { assertThat(4000, equalTo(image.height)) } - private fun saveTestBitmap( - width: Int, - height: Int, - attributes: Map = emptyMap() - ) { + private fun saveTestBitmap(width: Int, height: Int, attributes: Map = emptyMap()) { testImagePath = File.createTempFile("test", ".jpg").absolutePath val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565) ImageFileUtils.saveBitmapToFile(bitmap, testImagePath) - try { - val exifInterface = ExifInterface(testImagePath) - for ((key, value) in attributes) { - exifInterface.setAttribute(key, value) - } - exifInterface.saveAttributes() - } catch (e: IOException) { - Timber.w(e) + val exifInterface = ExifInterface(testImagePath) + for ((key, value) in attributes) { + exifInterface.setAttribute(key, value) } - } - - private fun getTestImageWidget(namespace: String, name: String, value: String): ImageWidget { - val bindAttributes: MutableList = mutableListOf() - bindAttributes.add(TreeElement.constructAttributeElement(namespace, name, value)) - return getTestImageWidget(bindAttributes) - } - - private fun getTestImageWidget(bindAttributes: List = emptyList()): ImageWidget { - val formEntryPrompt = mock(FormEntryPrompt::class.java) - `when`(formEntryPrompt.bindAttributes).thenReturn(bindAttributes) - val imageWidget = mock(ImageWidget::class.java) - `when`(imageWidget.formEntryPrompt).thenReturn(formEntryPrompt) - return imageWidget - } - - companion object { - private const val IMAGE_SIZE_ORIGINAL = "original_image_size" - private const val IMAGE_SIZE_LARGE = "large" - private const val IMAGE_SIZE_MEDIUM = "medium" - private const val IMAGE_SIZE_SMALL = "small" - private const val IMAGE_SIZE_VERY_SMALL = "very_small" + exifInterface.saveAttributes() } } diff --git a/collect_app/src/test/java/org/odk/collect/android/utilities/ImageCompressionControllerTest.kt b/collect_app/src/test/java/org/odk/collect/android/utilities/ImageCompressionControllerTest.kt index b69617016d5..9264583565b 100644 --- a/collect_app/src/test/java/org/odk/collect/android/utilities/ImageCompressionControllerTest.kt +++ b/collect_app/src/test/java/org/odk/collect/android/utilities/ImageCompressionControllerTest.kt @@ -1,5 +1,171 @@ package org.odk.collect.android.utilities -import org.junit.jupiter.api.Assertions.* +import android.app.Application +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.javarosa.core.model.instance.TreeElement +import org.javarosa.form.api.FormEntryPrompt +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoInteractions +import org.mockito.kotlin.whenever +import org.odk.collect.android.widgets.QuestionWidget -internal class ImageCompressionControllerTest \ No newline at end of file +@RunWith(AndroidJUnit4::class) +class ImageCompressionControllerTest { + + private val context = ApplicationProvider.getApplicationContext() + private val formEntryPrompt = mock() + private val questionWidget = mock().also { + whenever(it.formEntryPrompt).thenReturn(formEntryPrompt) + } + private val treeElement = mock().also { + whenever(it.attributeValue).thenReturn("123") + whenever(it.name).thenReturn("max-pixels") + whenever(it.namespace).thenReturn(ApplicationConstants.Namespaces.XML_OPENROSA_NAMESPACE) + } + private val imageCompressor = mock() + private val imageCompressionController = ImageCompressionController(imageCompressor) + + @Test + fun `when 'max-pixels' is not specified on a form level and expected image size in setting is 'original_image_size', image compression should not be triggered`() { + whenever(formEntryPrompt.bindAttributes).thenReturn(emptyList()) + + imageCompressionController.execute("/path", questionWidget, context, "original_image_size") + + verifyNoInteractions(imageCompressor) + } + + @Test + fun `when 'max-pixels' is not specified on a form level and expected image size in setting is 'very_small', image compression should be triggered for 640px`() { + whenever(formEntryPrompt.bindAttributes).thenReturn(emptyList()) + + imageCompressionController.execute("/path", questionWidget, context, "very_small") + + verify(imageCompressor).execute("/path", 640) + } + + @Test + fun `when 'max-pixels' is not specified on a form level and expected image size in setting is 'small', image compression should be triggered for 1024px`() { + whenever(formEntryPrompt.bindAttributes).thenReturn(emptyList()) + + imageCompressionController.execute("/path", questionWidget, context, "small") + + verify(imageCompressor).execute("/path", 1024) + } + + @Test + fun `when 'max-pixels' is not specified on a form level and expected image size in setting is 'medium', image compression should be triggered for 2048px`() { + whenever(formEntryPrompt.bindAttributes).thenReturn(emptyList()) + + imageCompressionController.execute("/path", questionWidget, context, "medium") + + verify(imageCompressor).execute("/path", 2048) + } + + @Test + fun `when 'max-pixels' is not specified on a form level and expected image size in setting is 'large', image compression should be triggered for 3072px`() { + whenever(formEntryPrompt.bindAttributes).thenReturn(emptyList()) + + imageCompressionController.execute("/path", questionWidget, context, "large") + + verify(imageCompressor).execute("/path", 3072) + } + + @Test + fun `when 'max-pixels' is specified on a form level and expected image size in setting is 'original_image_size', image compression should be triggered for value stored in 'max-pixels'`() { + whenever(formEntryPrompt.bindAttributes).thenReturn(listOf(treeElement)) + + imageCompressionController.execute("/path", questionWidget, context, "original_image_size") + + verify(imageCompressor).execute("/path", 123) + } + + @Test + fun `when 'max-pixels' is specified on a form level and expected image size in setting is 'very_small', image compression should be triggered and the value stored in for value stored in 'max-pixels' should take precedence`() { + whenever(formEntryPrompt.bindAttributes).thenReturn(listOf(treeElement)) + + imageCompressionController.execute("/path", questionWidget, context, "very_small") + + verify(imageCompressor).execute("/path", 123) + } + + @Test + fun `when 'max-pixels' is specified on a form level and expected image size in setting is 'small', image compression should be triggered and the value stored in for value stored in 'max-pixels' should take precedence`() { + whenever(formEntryPrompt.bindAttributes).thenReturn(listOf(treeElement)) + + imageCompressionController.execute("/path", questionWidget, context, "small") + + verify(imageCompressor).execute("/path", 123) + } + + @Test + fun `when 'max-pixels' is specified on a form level and expected image size in setting is 'medium', image compression should be triggered and the value stored in for value stored in 'max-pixels' should take precedence`() { + whenever(formEntryPrompt.bindAttributes).thenReturn(listOf(treeElement)) + + imageCompressionController.execute("/path", questionWidget, context, "medium") + + verify(imageCompressor).execute("/path", 123) + } + + @Test + fun `when 'max-pixels' is specified on a form level and expected image size in setting is 'large', image compression should be triggered and the value stored in for value stored in 'max-pixels' should take precedence`() { + whenever(formEntryPrompt.bindAttributes).thenReturn(listOf(treeElement)) + + imageCompressionController.execute("/path", questionWidget, context, "large") + + verify(imageCompressor).execute("/path", 123) + } + + @Test + fun `when 'max-pixels' is specified on a form level but it is not a valid integer value and expected image size in setting is 'original_image_size', image compression should not be triggered`() { + whenever(treeElement.attributeValue).thenReturn("123.5") + whenever(formEntryPrompt.bindAttributes).thenReturn(listOf(treeElement)) + + imageCompressionController.execute("/path", questionWidget, context, "original_image_size") + + verifyNoInteractions(imageCompressor) + } + + @Test + fun `when 'max-pixels' is specified on a form level but it is not a valid integer value and expected image size in setting is 'very_small', image compression should be triggered for 640px`() { + whenever(treeElement.attributeValue).thenReturn("123.5") + whenever(formEntryPrompt.bindAttributes).thenReturn(listOf(treeElement)) + + imageCompressionController.execute("/path", questionWidget, context, "very_small") + + verify(imageCompressor).execute("/path", 640) + } + + @Test + fun `when 'max-pixels' is specified on a form level but it is not a valid integer value and expected image size in setting is 'small', image compression should be triggered for 1024px`() { + whenever(treeElement.attributeValue).thenReturn("123.5") + whenever(formEntryPrompt.bindAttributes).thenReturn(listOf(treeElement)) + + imageCompressionController.execute("/path", questionWidget, context, "small") + + verify(imageCompressor).execute("/path", 1024) + } + + @Test + fun `when 'max-pixels' is specified on a form level but it is not a valid integer value and expected image size in setting is 'medium', image compression should be triggered for 2048px`() { + whenever(treeElement.attributeValue).thenReturn("123.5") + whenever(formEntryPrompt.bindAttributes).thenReturn(listOf(treeElement)) + + imageCompressionController.execute("/path", questionWidget, context, "medium") + + verify(imageCompressor).execute("/path", 2048) + } + + @Test + fun `when 'max-pixels' is specified on a form level but it is not a valid integer value and expected image size in setting is 'large', image compression should be triggered for 3072px`() { + whenever(treeElement.attributeValue).thenReturn("123.5") + whenever(formEntryPrompt.bindAttributes).thenReturn(listOf(treeElement)) + + imageCompressionController.execute("/path", questionWidget, context, "large") + + verify(imageCompressor).execute("/path", 3072) + } +} From 35d8f95db03e9b9e68e2c75b98548a33ef1a43d8 Mon Sep 17 00:00:00 2001 From: Grzegorz Orczykowski Date: Fri, 24 Feb 2023 13:06:21 +0100 Subject: [PATCH 6/6] Moved code responsible for dealing with bitmaps to androidshared --- androidshared/build.gradle | 4 +++ .../bitmap}/ImageCompressorTest.kt | 6 ++-- .../bitmap}/ImageFileUtilsTest.kt | 28 ++++++++++++++----- .../androidshared/bitmap}/ImageCompressor.kt | 2 +- .../androidshared/bitmap}/ImageFileUtils.kt | 4 +-- collect_app/build.gradle | 1 - .../configure/qr/CachingQRCodeGenerator.kt | 4 +-- .../android/configure/qr/QRCodeViewModel.kt | 4 +-- .../collect/android/draw/DrawActivity.java | 2 +- .../org/odk/collect/android/draw/DrawView.kt | 2 +- .../injection/config/AppDependencyModule.java | 2 +- .../utilities/ImageCompressionController.kt | 1 + .../android/widgets/items/LabelWidget.java | 2 +- .../android/widgets/items/LikertWidget.java | 2 +- .../widgets/items/ListMultiWidget.java | 2 +- .../android/widgets/items/ListWidget.java | 2 +- .../ImageCompressionControllerTest.kt | 1 + 17 files changed, 43 insertions(+), 26 deletions(-) rename {collect_app/src/androidTest/java/org/odk/collect/android/instrumented/utilities => androidshared/src/androidTest/java/org/odk/collect/androidshared/bitmap}/ImageCompressorTest.kt (97%) rename {collect_app/src/androidTest/java/org/odk/collect/android/instrumented/utilities => androidshared/src/androidTest/java/org/odk/collect/androidshared/bitmap}/ImageFileUtilsTest.kt (92%) rename {collect_app/src/main/java/org/odk/collect/android/utilities => androidshared/src/main/java/org/odk/collect/androidshared/bitmap}/ImageCompressor.kt (99%) rename {collect_app/src/main/java/org/odk/collect/android/utilities => androidshared/src/main/java/org/odk/collect/androidshared/bitmap}/ImageFileUtils.kt (98%) diff --git a/androidshared/build.gradle b/androidshared/build.gradle index 6671d6c56ac..e420bcb0a0c 100644 --- a/androidshared/build.gradle +++ b/androidshared/build.gradle @@ -62,6 +62,7 @@ dependencies { implementation Dependencies.androidx_fragment_ktx implementation Dependencies.androidx_preference_ktx implementation Dependencies.timber + implementation Dependencies.androidx_exinterface testImplementation project(':testshared') testImplementation Dependencies.junit @@ -70,5 +71,8 @@ dependencies { testImplementation Dependencies.robolectric testImplementation Dependencies.mockito_kotlin + androidTestImplementation Dependencies.androidx_test_ext_junit + androidTestImplementation Dependencies.junit + debugImplementation project(':fragmentstest') } diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/instrumented/utilities/ImageCompressorTest.kt b/androidshared/src/androidTest/java/org/odk/collect/androidshared/bitmap/ImageCompressorTest.kt similarity index 97% rename from collect_app/src/androidTest/java/org/odk/collect/android/instrumented/utilities/ImageCompressorTest.kt rename to androidshared/src/androidTest/java/org/odk/collect/androidshared/bitmap/ImageCompressorTest.kt index d215bd57461..866a171d49f 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/instrumented/utilities/ImageCompressorTest.kt +++ b/androidshared/src/androidTest/java/org/odk/collect/androidshared/bitmap/ImageCompressorTest.kt @@ -13,18 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.odk.collect.android.instrumented.utilities +package org.odk.collect.androidshared.bitmap import android.graphics.Bitmap import android.graphics.BitmapFactory import androidx.exifinterface.media.ExifInterface import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.hamcrest.CoreMatchers.equalTo import org.hamcrest.MatcherAssert.assertThat -import org.hamcrest.Matchers.equalTo import org.junit.Test import org.junit.runner.RunWith -import org.odk.collect.android.utilities.ImageCompressor -import org.odk.collect.android.utilities.ImageFileUtils import java.io.File @RunWith(AndroidJUnit4::class) diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/instrumented/utilities/ImageFileUtilsTest.kt b/androidshared/src/androidTest/java/org/odk/collect/androidshared/bitmap/ImageFileUtilsTest.kt similarity index 92% rename from collect_app/src/androidTest/java/org/odk/collect/android/instrumented/utilities/ImageFileUtilsTest.kt rename to androidshared/src/androidTest/java/org/odk/collect/androidshared/bitmap/ImageFileUtilsTest.kt index 31b64f74778..749f5bea83a 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/instrumented/utilities/ImageFileUtilsTest.kt +++ b/androidshared/src/androidTest/java/org/odk/collect/androidshared/bitmap/ImageFileUtilsTest.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.odk.collect.android.instrumented.utilities +package org.odk.collect.androidshared.bitmap import android.content.Context import android.graphics.Bitmap @@ -26,7 +26,6 @@ import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.odk.collect.android.utilities.ImageFileUtils import timber.log.Timber import java.io.File import java.io.IOException @@ -50,7 +49,10 @@ class ImageFileUtilsTest { attributes[ExifInterface.TAG_ORIENTATION] = ExifInterface.ORIENTATION_ROTATE_90.toString() saveTestBitmapToFile(sourceFile.absolutePath, attributes) ImageFileUtils.copyImageAndApplyExifRotation(sourceFile, destinationFile) - val image = ImageFileUtils.getBitmap(destinationFile.absolutePath, BitmapFactory.Options())!! + val image = ImageFileUtils.getBitmap( + destinationFile.absolutePath, + BitmapFactory.Options() + )!! assertEquals(2, image.width) assertEquals(1, image.height) @@ -64,7 +66,10 @@ class ImageFileUtilsTest { attributes[ExifInterface.TAG_ORIENTATION] = ExifInterface.ORIENTATION_ROTATE_270.toString() saveTestBitmapToFile(sourceFile.absolutePath, attributes) ImageFileUtils.copyImageAndApplyExifRotation(sourceFile, destinationFile) - val image = ImageFileUtils.getBitmap(destinationFile.absolutePath, BitmapFactory.Options())!! + val image = ImageFileUtils.getBitmap( + destinationFile.absolutePath, + BitmapFactory.Options() + )!! assertEquals(2, image.width) assertEquals(1, image.height) @@ -78,7 +83,10 @@ class ImageFileUtilsTest { attributes[ExifInterface.TAG_ORIENTATION] = ExifInterface.ORIENTATION_ROTATE_180.toString() saveTestBitmapToFile(sourceFile.absolutePath, attributes) ImageFileUtils.copyImageAndApplyExifRotation(sourceFile, destinationFile) - val image = ImageFileUtils.getBitmap(destinationFile.absolutePath, BitmapFactory.Options())!! + val image = ImageFileUtils.getBitmap( + destinationFile.absolutePath, + BitmapFactory.Options() + )!! assertEquals(1, image.width) assertEquals(2, image.height) @@ -92,7 +100,10 @@ class ImageFileUtilsTest { attributes[ExifInterface.TAG_ORIENTATION] = ExifInterface.ORIENTATION_UNDEFINED.toString() saveTestBitmapToFile(sourceFile.absolutePath, attributes) ImageFileUtils.copyImageAndApplyExifRotation(sourceFile, destinationFile) - val image = ImageFileUtils.getBitmap(destinationFile.absolutePath, BitmapFactory.Options())!! + val image = ImageFileUtils.getBitmap( + destinationFile.absolutePath, + BitmapFactory.Options() + )!! assertEquals(1, image.width) assertEquals(2, image.height) @@ -105,7 +116,10 @@ class ImageFileUtilsTest { fun copyAndRotateImageNoExif() { saveTestBitmapToFile(sourceFile.absolutePath, null) ImageFileUtils.copyImageAndApplyExifRotation(sourceFile, destinationFile) - val image = ImageFileUtils.getBitmap(destinationFile.absolutePath, BitmapFactory.Options())!! + val image = ImageFileUtils.getBitmap( + destinationFile.absolutePath, + BitmapFactory.Options() + )!! assertEquals(1, image.width) assertEquals(2, image.height) diff --git a/collect_app/src/main/java/org/odk/collect/android/utilities/ImageCompressor.kt b/androidshared/src/main/java/org/odk/collect/androidshared/bitmap/ImageCompressor.kt similarity index 99% rename from collect_app/src/main/java/org/odk/collect/android/utilities/ImageCompressor.kt rename to androidshared/src/main/java/org/odk/collect/androidshared/bitmap/ImageCompressor.kt index 4d9a52fbede..e5e94b283b9 100644 --- a/collect_app/src/main/java/org/odk/collect/android/utilities/ImageCompressor.kt +++ b/androidshared/src/main/java/org/odk/collect/androidshared/bitmap/ImageCompressor.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.odk.collect.android.utilities +package org.odk.collect.androidshared.bitmap import android.graphics.Bitmap import android.graphics.BitmapFactory diff --git a/collect_app/src/main/java/org/odk/collect/android/utilities/ImageFileUtils.kt b/androidshared/src/main/java/org/odk/collect/androidshared/bitmap/ImageFileUtils.kt similarity index 98% rename from collect_app/src/main/java/org/odk/collect/android/utilities/ImageFileUtils.kt rename to androidshared/src/main/java/org/odk/collect/androidshared/bitmap/ImageFileUtils.kt index 753e94cef6e..fec44ca3478 100644 --- a/collect_app/src/main/java/org/odk/collect/android/utilities/ImageFileUtils.kt +++ b/androidshared/src/main/java/org/odk/collect/androidshared/bitmap/ImageFileUtils.kt @@ -1,4 +1,4 @@ -package org.odk.collect.android.utilities +package org.odk.collect.androidshared.bitmap import android.graphics.Bitmap import android.graphics.Bitmap.CompressFormat @@ -150,7 +150,7 @@ object ImageFileUtils { ) ) { // Source Image doesn't have any EXIF Rotations, so a normal file copy will suffice - FileUtils.copyFile(sourceFile, destFile) + sourceFile.copyTo(destFile, true) } else { val sourceImage = getBitmap(sourceFile.absolutePath, BitmapFactory.Options()) val orientation = sourceFileExif.getAttributeInt( diff --git a/collect_app/build.gradle b/collect_app/build.gradle index 5153f200e84..2b1ae85bef0 100644 --- a/collect_app/build.gradle +++ b/collect_app/build.gradle @@ -280,7 +280,6 @@ dependencies { implementation Dependencies.androidx_work_runtime implementation Dependencies.androidx_cardview - implementation Dependencies.androidx_exinterface implementation Dependencies.androidx_multidex implementation Dependencies.androidx_preference_ktx implementation Dependencies.androidx_fragment_ktx diff --git a/collect_app/src/main/java/org/odk/collect/android/configure/qr/CachingQRCodeGenerator.kt b/collect_app/src/main/java/org/odk/collect/android/configure/qr/CachingQRCodeGenerator.kt index 135b6b33be5..090a549fc3e 100644 --- a/collect_app/src/main/java/org/odk/collect/android/configure/qr/CachingQRCodeGenerator.kt +++ b/collect_app/src/main/java/org/odk/collect/android/configure/qr/CachingQRCodeGenerator.kt @@ -5,7 +5,7 @@ import org.json.JSONException import org.odk.collect.android.storage.StoragePathProvider import org.odk.collect.android.storage.StorageSubdirectory import org.odk.collect.android.utilities.FileUtils -import org.odk.collect.android.utilities.ImageFileUtils.saveBitmapToFile +import org.odk.collect.androidshared.bitmap.ImageFileUtils import org.odk.collect.qrcode.QRCodeDecoder import org.odk.collect.qrcode.QRCodeEncoder import timber.log.Timber @@ -62,7 +62,7 @@ class CachingQRCodeGenerator(private val qrCodeEncoder: QRCodeEncoder) : QRCodeG val bmp = qrCodeEncoder.encode(preferencesString) Timber.i("QR Code generation took : %d ms", System.currentTimeMillis() - time) Timber.i("Saving QR Code to disk... : %s", qRCodeFilepath) - saveBitmapToFile(bmp, qRCodeFilepath) + ImageFileUtils.saveBitmapToFile(bmp, qRCodeFilepath) FileUtils.write(mdCacheFile, messageDigest) Timber.i("Updated %s file contents", SETTINGS_MD5_FILE) } diff --git a/collect_app/src/main/java/org/odk/collect/android/configure/qr/QRCodeViewModel.kt b/collect_app/src/main/java/org/odk/collect/android/configure/qr/QRCodeViewModel.kt index f94fbfd61c8..3d903cc430f 100644 --- a/collect_app/src/main/java/org/odk/collect/android/configure/qr/QRCodeViewModel.kt +++ b/collect_app/src/main/java/org/odk/collect/android/configure/qr/QRCodeViewModel.kt @@ -8,7 +8,7 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import org.odk.collect.android.R -import org.odk.collect.android.utilities.ImageFileUtils.getBitmap +import org.odk.collect.androidshared.bitmap.ImageFileUtils import org.odk.collect.async.Scheduler import org.odk.collect.settings.SettingsProvider import org.odk.collect.settings.keys.ProjectKeys @@ -50,7 +50,7 @@ class QRCodeViewModel( qrCodeGenerator.generateQRCode(includedKeys, appConfigurationGenerator) val options = BitmapFactory.Options() options.inPreferredConfig = Bitmap.Config.ARGB_8888 - val bitmap = getBitmap(filePath, options) + val bitmap = ImageFileUtils.getBitmap(filePath, options) return@immediate Pair(filePath, bitmap) } catch (ignored: Exception) { // Ignored diff --git a/collect_app/src/main/java/org/odk/collect/android/draw/DrawActivity.java b/collect_app/src/main/java/org/odk/collect/android/draw/DrawActivity.java index ad090462991..f6457163eaf 100644 --- a/collect_app/src/main/java/org/odk/collect/android/draw/DrawActivity.java +++ b/collect_app/src/main/java/org/odk/collect/android/draw/DrawActivity.java @@ -46,7 +46,7 @@ import org.odk.collect.android.utilities.AnimationUtils; import org.odk.collect.android.storage.StoragePathProvider; import org.odk.collect.android.utilities.DialogUtils; -import org.odk.collect.android.utilities.ImageFileUtils; +import org.odk.collect.androidshared.bitmap.ImageFileUtils; import org.odk.collect.androidshared.ui.DialogFragmentUtils; import org.odk.collect.strings.localization.LocalizedActivity; diff --git a/collect_app/src/main/java/org/odk/collect/android/draw/DrawView.kt b/collect_app/src/main/java/org/odk/collect/android/draw/DrawView.kt index 607ed48d376..5d28f7e9c02 100644 --- a/collect_app/src/main/java/org/odk/collect/android/draw/DrawView.kt +++ b/collect_app/src/main/java/org/odk/collect/android/draw/DrawView.kt @@ -25,7 +25,7 @@ import android.util.AttributeSet import android.view.MotionEvent import android.view.View import org.odk.collect.android.storage.StoragePathProvider -import org.odk.collect.android.utilities.ImageFileUtils +import org.odk.collect.androidshared.bitmap.ImageFileUtils import java.io.File class DrawView(context: Context?, attrs: AttributeSet?) : View(context, attrs) { diff --git a/collect_app/src/main/java/org/odk/collect/android/injection/config/AppDependencyModule.java b/collect_app/src/main/java/org/odk/collect/android/injection/config/AppDependencyModule.java index f33e5586e45..9ecdddec4ed 100644 --- a/collect_app/src/main/java/org/odk/collect/android/injection/config/AppDependencyModule.java +++ b/collect_app/src/main/java/org/odk/collect/android/injection/config/AppDependencyModule.java @@ -111,7 +111,7 @@ import org.odk.collect.android.utilities.FileProvider; import org.odk.collect.android.utilities.FormsDirDiskFormsSynchronizer; import org.odk.collect.android.utilities.FormsRepositoryProvider; -import org.odk.collect.android.utilities.ImageCompressor; +import org.odk.collect.androidshared.bitmap.ImageCompressor; import org.odk.collect.android.utilities.ImageCompressionController; import org.odk.collect.android.utilities.InstancesRepositoryProvider; import org.odk.collect.android.utilities.MediaUtils; diff --git a/collect_app/src/main/java/org/odk/collect/android/utilities/ImageCompressionController.kt b/collect_app/src/main/java/org/odk/collect/android/utilities/ImageCompressionController.kt index 611e5317424..a4953c82e68 100644 --- a/collect_app/src/main/java/org/odk/collect/android/utilities/ImageCompressionController.kt +++ b/collect_app/src/main/java/org/odk/collect/android/utilities/ImageCompressionController.kt @@ -3,6 +3,7 @@ package org.odk.collect.android.utilities import android.content.Context import org.odk.collect.android.R import org.odk.collect.android.widgets.QuestionWidget +import org.odk.collect.androidshared.bitmap.ImageCompressor import timber.log.Timber class ImageCompressionController(private val imageCompressor: ImageCompressor) { diff --git a/collect_app/src/main/java/org/odk/collect/android/widgets/items/LabelWidget.java b/collect_app/src/main/java/org/odk/collect/android/widgets/items/LabelWidget.java index c1d2aa2bf1d..b230ba4f777 100644 --- a/collect_app/src/main/java/org/odk/collect/android/widgets/items/LabelWidget.java +++ b/collect_app/src/main/java/org/odk/collect/android/widgets/items/LabelWidget.java @@ -35,7 +35,7 @@ import org.odk.collect.android.externaldata.ExternalSelectChoice; import org.odk.collect.android.formentry.questions.QuestionDetails; import org.odk.collect.android.utilities.HtmlUtils; -import org.odk.collect.android.utilities.ImageFileUtils; +import org.odk.collect.androidshared.bitmap.ImageFileUtils; import org.odk.collect.android.widgets.QuestionWidget; import org.odk.collect.android.widgets.interfaces.SelectChoiceLoader; import org.odk.collect.android.widgets.warnings.SpacesInUnderlyingValuesWarning; diff --git a/collect_app/src/main/java/org/odk/collect/android/widgets/items/LikertWidget.java b/collect_app/src/main/java/org/odk/collect/android/widgets/items/LikertWidget.java index 7b16ffcc4ba..a3e999ad926 100644 --- a/collect_app/src/main/java/org/odk/collect/android/widgets/items/LikertWidget.java +++ b/collect_app/src/main/java/org/odk/collect/android/widgets/items/LikertWidget.java @@ -32,7 +32,7 @@ import org.odk.collect.android.externaldata.ExternalSelectChoice; import org.odk.collect.android.formentry.questions.QuestionDetails; import org.odk.collect.android.utilities.HtmlUtils; -import org.odk.collect.android.utilities.ImageFileUtils; +import org.odk.collect.androidshared.bitmap.ImageFileUtils; import org.odk.collect.android.widgets.QuestionWidget; import org.odk.collect.android.widgets.interfaces.SelectChoiceLoader; diff --git a/collect_app/src/main/java/org/odk/collect/android/widgets/items/ListMultiWidget.java b/collect_app/src/main/java/org/odk/collect/android/widgets/items/ListMultiWidget.java index 900a12aefcd..feacec0d651 100644 --- a/collect_app/src/main/java/org/odk/collect/android/widgets/items/ListMultiWidget.java +++ b/collect_app/src/main/java/org/odk/collect/android/widgets/items/ListMultiWidget.java @@ -42,7 +42,7 @@ import org.odk.collect.android.externaldata.ExternalSelectChoice; import org.odk.collect.android.formentry.questions.QuestionDetails; import org.odk.collect.android.utilities.HtmlUtils; -import org.odk.collect.android.utilities.ImageFileUtils; +import org.odk.collect.androidshared.bitmap.ImageFileUtils; import org.odk.collect.android.widgets.QuestionWidget; import org.odk.collect.android.widgets.interfaces.MultiChoiceWidget; import org.odk.collect.android.widgets.interfaces.SelectChoiceLoader; diff --git a/collect_app/src/main/java/org/odk/collect/android/widgets/items/ListWidget.java b/collect_app/src/main/java/org/odk/collect/android/widgets/items/ListWidget.java index b51f453dfe7..f266baef14f 100644 --- a/collect_app/src/main/java/org/odk/collect/android/widgets/items/ListWidget.java +++ b/collect_app/src/main/java/org/odk/collect/android/widgets/items/ListWidget.java @@ -44,7 +44,7 @@ import org.odk.collect.android.formentry.questions.QuestionDetails; import org.odk.collect.android.listeners.AdvanceToNextListener; import org.odk.collect.android.utilities.HtmlUtils; -import org.odk.collect.android.utilities.ImageFileUtils; +import org.odk.collect.androidshared.bitmap.ImageFileUtils; import org.odk.collect.android.utilities.SelectOneWidgetUtils; import org.odk.collect.android.widgets.QuestionWidget; import org.odk.collect.android.widgets.interfaces.MultiChoiceWidget; diff --git a/collect_app/src/test/java/org/odk/collect/android/utilities/ImageCompressionControllerTest.kt b/collect_app/src/test/java/org/odk/collect/android/utilities/ImageCompressionControllerTest.kt index 9264583565b..a285767fa30 100644 --- a/collect_app/src/test/java/org/odk/collect/android/utilities/ImageCompressionControllerTest.kt +++ b/collect_app/src/test/java/org/odk/collect/android/utilities/ImageCompressionControllerTest.kt @@ -12,6 +12,7 @@ import org.mockito.kotlin.verify import org.mockito.kotlin.verifyNoInteractions import org.mockito.kotlin.whenever import org.odk.collect.android.widgets.QuestionWidget +import org.odk.collect.androidshared.bitmap.ImageCompressor @RunWith(AndroidJUnit4::class) class ImageCompressionControllerTest {