diff --git a/app/src/androidTest/java/WorkerInstrumentationTest.kt b/app/src/androidTest/java/WorkerInstrumentationTest.kt index ca868e1..ded5bdf 100644 --- a/app/src/androidTest/java/WorkerInstrumentationTest.kt +++ b/app/src/androidTest/java/WorkerInstrumentationTest.kt @@ -15,6 +15,7 @@ */ import android.content.Context +import android.os.Build import androidx.test.core.app.ApplicationProvider import androidx.work.ListenableWorker import androidx.work.testing.TestListenableWorkerBuilder @@ -23,6 +24,7 @@ import com.example.bluromatic.KEY_IMAGE_URI import com.example.bluromatic.workers.BlurWorker import com.example.bluromatic.workers.CleanupWorker import com.example.bluromatic.workers.SaveImageToFileWorker +import junit.framework.TestCase import kotlinx.coroutines.runBlocking import org.junit.Assert.assertTrue import org.junit.Before @@ -75,10 +77,17 @@ class WorkerInstrumentationTest { val resultUri = result.outputData.getString(KEY_IMAGE_URI) assertTrue(result is ListenableWorker.Result.Success) assertTrue(result.outputData.keyValueMap.containsKey(KEY_IMAGE_URI)) - assertTrue( - resultUri?.startsWith("content://media/external/images/media/") - ?: false - ) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + TestCase.assertTrue( + resultUri?.startsWith("content://media/external_primary/images/media/") + ?: false + ) + } else { + TestCase.assertTrue( + resultUri?.startsWith("content://media/external/images/media/") + ?: false + ) + } } } } diff --git a/app/src/main/java/com/example/bluromatic/domain/CreateImageUriUseCase.kt b/app/src/main/java/com/example/bluromatic/domain/CreateImageUriUseCase.kt new file mode 100644 index 0000000..9a0151d --- /dev/null +++ b/app/src/main/java/com/example/bluromatic/domain/CreateImageUriUseCase.kt @@ -0,0 +1,30 @@ +package com.example.bluromatic.domain + +import android.content.ContentResolver +import android.content.ContentValues +import android.net.Uri +import android.provider.MediaStore +import androidx.annotation.RequiresApi +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +/** + * Create imageUri. + */ +@RequiresApi(29) +class CreateImageUriUseCase { + suspend operator fun invoke(resolver: ContentResolver, filename: String): Uri? { + return withContext(Dispatchers.IO) { + val imageCollection = + MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) + val values = ContentValues().apply { + put(MediaStore.Images.Media.DISPLAY_NAME, filename) + put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/Blur-O-Matic") + put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg") + put(MediaStore.Images.Media.IS_PENDING, 1) + } + val imageUri = resolver.insert(imageCollection, values) + imageUri + } + } +} diff --git a/app/src/main/java/com/example/bluromatic/domain/SaveImageUseCase.kt b/app/src/main/java/com/example/bluromatic/domain/SaveImageUseCase.kt new file mode 100644 index 0000000..8af2e50 --- /dev/null +++ b/app/src/main/java/com/example/bluromatic/domain/SaveImageUseCase.kt @@ -0,0 +1,38 @@ +package com.example.bluromatic.domain + +import android.content.ContentResolver +import android.content.ContentValues +import android.graphics.Bitmap +import android.net.Uri +import android.provider.MediaStore +import android.util.Log +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +private const val TAG = "SaveImage" + +/** + * Save image into MediaStore. + */ +class SaveImageUseCase { + suspend operator fun invoke( + resolver: ContentResolver, + contentUri: Uri?, + bitmap: Bitmap, + ) = withContext(Dispatchers.IO) { + try { + contentUri?.let { contentUri -> + resolver.openOutputStream(contentUri, "w")?.use { outputStream -> + bitmap.compress(Bitmap.CompressFormat.JPEG, 85, outputStream) + } + val values = ContentValues().apply { + put(MediaStore.Images.Media.IS_PENDING, 0) + } + resolver.update(contentUri, values, null, null) + } + } catch (e: Throwable) { + Log.e(TAG, "Error occurs $e") + throw e + } + } +} diff --git a/app/src/main/java/com/example/bluromatic/workers/SaveImageToFileWorker.kt b/app/src/main/java/com/example/bluromatic/workers/SaveImageToFileWorker.kt index 6324eac..c658a19 100644 --- a/app/src/main/java/com/example/bluromatic/workers/SaveImageToFileWorker.kt +++ b/app/src/main/java/com/example/bluromatic/workers/SaveImageToFileWorker.kt @@ -19,6 +19,7 @@ package com.example.bluromatic.workers import android.content.Context import android.graphics.BitmapFactory import android.net.Uri +import android.os.Build import android.provider.MediaStore import android.util.Log import androidx.work.CoroutineWorker @@ -27,6 +28,8 @@ import androidx.work.workDataOf import com.example.bluromatic.DELAY_TIME_MILLIS import com.example.bluromatic.KEY_IMAGE_URI import com.example.bluromatic.R +import com.example.bluromatic.domain.CreateImageUriUseCase +import com.example.bluromatic.domain.SaveImageUseCase import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.withContext @@ -64,9 +67,25 @@ class SaveImageToFileWorker(ctx: Context, params: WorkerParameters) : CoroutineW val bitmap = BitmapFactory.decodeStream( resolver.openInputStream(Uri.parse(resourceUri)) ) - val imageUrl = MediaStore.Images.Media.insertImage( - resolver, bitmap, title, dateFormatter.format(Date()) - ) + + val imageUrl: String? + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + val createImageUriUseCase = CreateImageUriUseCase() + val saveImageUseCase = SaveImageUseCase() + + val imageUri = createImageUriUseCase( + resolver, + "${title}_${dateFormatter.format(Date())}.jpg" + ) + + saveImageUseCase(resolver, imageUri, bitmap) + imageUrl = imageUri.toString() + } else { + @Suppress("DEPRECATION") + imageUrl = MediaStore.Images.Media.insertImage( + resolver, bitmap, title, dateFormatter.format(Date()) + ) + } if (!imageUrl.isNullOrEmpty()) { val output = workDataOf(KEY_IMAGE_URI to imageUrl)