From 6ae0752a4624e23060099f19106ab39ad6b48995 Mon Sep 17 00:00:00 2001 From: Alexey Nesterov Date: Tue, 7 May 2024 11:53:04 +0700 Subject: [PATCH] #34 Add base PickerDelegate class --- .../moko/media/picker/CameraPickerDelegate.kt | 72 ++++++++ .../moko/media/picker/FilePickerDelegate.kt | 89 +++++----- .../media/picker/GalleryPickerDelegate.kt | 70 ++++++++ .../moko/media/picker/ImagePickerDelegate.kt | 163 +----------------- .../media/picker/MediaPickerControllerImpl.kt | 18 +- .../moko/media/picker/MediaPickerDelegate.kt | 67 +++---- .../moko/media/picker/PickerDelegate.kt | 52 ++++++ 7 files changed, 272 insertions(+), 259 deletions(-) create mode 100644 media/src/androidMain/kotlin/dev/icerock/moko/media/picker/CameraPickerDelegate.kt create mode 100644 media/src/androidMain/kotlin/dev/icerock/moko/media/picker/GalleryPickerDelegate.kt create mode 100644 media/src/androidMain/kotlin/dev/icerock/moko/media/picker/PickerDelegate.kt diff --git a/media/src/androidMain/kotlin/dev/icerock/moko/media/picker/CameraPickerDelegate.kt b/media/src/androidMain/kotlin/dev/icerock/moko/media/picker/CameraPickerDelegate.kt new file mode 100644 index 0000000..8957a8b --- /dev/null +++ b/media/src/androidMain/kotlin/dev/icerock/moko/media/picker/CameraPickerDelegate.kt @@ -0,0 +1,72 @@ +/* + * Copyright 2024 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.media.picker + +import android.content.Context +import android.net.Uri +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.ActivityResultRegistry +import androidx.activity.result.contract.ActivityResultContracts + +internal class CameraPickerDelegate : + ImagePickerDelegate() { + + override fun registerActivityResult( + context: Context, + activityResultRegistry: ActivityResultRegistry + ): ActivityResultLauncher = activityResultRegistry.register( + PICK_CAMERA_IMAGE_KEY, + ActivityResultContracts.TakePicture(), + ) { result -> + val callbackData = callback ?: return@register + callback = null + + if (!result) { + callbackData.callback.invoke(Result.failure(CanceledException())) + return@register + } + + processResult( + context = context, + callback = callbackData.callback, + uri = callbackData.outputUri, + maxImageWidth = callbackData.maxWidth, + maxImageHeight = callbackData.maxHeight, + ) + } + + fun pick( + maxWidth: Int, + maxHeight: Int, + callback: (Result) -> Unit, + outputUri: Uri, + ) { + this.callback?.let { + it.callback.invoke(Result.failure(IllegalStateException("Callback should be null"))) + this.callback = null + } + this.callback = CallbackData( + callback, + outputUri, + maxWidth, + maxHeight, + ) + + pickerLauncherHolder.value?.launch( + outputUri + ) + } + + class CallbackData( + val callback: (Result) -> Unit, + val outputUri: Uri, + val maxWidth: Int, + val maxHeight: Int, + ) + + companion object { + private const val PICK_CAMERA_IMAGE_KEY = "PickCameraImageKey" + } +} diff --git a/media/src/androidMain/kotlin/dev/icerock/moko/media/picker/FilePickerDelegate.kt b/media/src/androidMain/kotlin/dev/icerock/moko/media/picker/FilePickerDelegate.kt index 1fc627c..6a24f7e 100644 --- a/media/src/androidMain/kotlin/dev/icerock/moko/media/picker/FilePickerDelegate.kt +++ b/media/src/androidMain/kotlin/dev/icerock/moko/media/picker/FilePickerDelegate.kt @@ -6,68 +6,61 @@ package dev.icerock.moko.media.picker import android.annotation.SuppressLint import android.content.ContentResolver +import android.content.Context import android.provider.OpenableColumns -import androidx.activity.ComponentActivity import androidx.activity.result.ActivityResultLauncher -import androidx.activity.result.ActivityResultRegistryOwner +import androidx.activity.result.ActivityResultRegistry import androidx.activity.result.contract.ActivityResultContracts import dev.icerock.moko.media.FileMedia -import kotlinx.coroutines.flow.MutableStateFlow import java.io.File -internal class FilePickerDelegate { +internal class FilePickerDelegate : + PickerDelegate>() { - private var callback: CallbackData? = null + override fun registerActivityResult( + context: Context, + activityResultRegistry: ActivityResultRegistry + ): ActivityResultLauncher> = activityResultRegistry.register( + PICK_FILE_KEY, + ActivityResultContracts.OpenDocument(), + ) { uri -> + val callbackData = callback ?: return@register + callback = null - private val filePickerLauncherHolder = - MutableStateFlow>?>(null) + val callback = callbackData.callback - fun bind(activity: ComponentActivity) { - val activityResultRegistryOwner = activity as ActivityResultRegistryOwner - val activityResultRegistry = activityResultRegistryOwner.activityResultRegistry - - filePickerLauncherHolder.value = activityResultRegistry.register( - PICK_FILE_KEY, - ActivityResultContracts.OpenDocument(), - ) { uri -> - val callbackData = callback ?: return@register - callback = null - - val callback = callbackData.callback + if (uri == null) { + callback.invoke(Result.failure(CanceledException())) + return@register + } - if (uri == null) { - callback.invoke(Result.failure(CanceledException())) - return@register - } + val path = uri.path + if (path == null) { + callback.invoke(Result.failure(java.lang.IllegalStateException("File is null"))) + return@register + } - val path = uri.path - if (path == null) { - callback.invoke(Result.failure(java.lang.IllegalStateException("File is null"))) - return@register + @SuppressLint("Range") + val fileNameWithExtension = if (uri.scheme == ContentResolver.SCHEME_CONTENT) { + val cursor = context.contentResolver.query(uri, null, null, null, null) + cursor?.use { + if (!it.moveToFirst()) null + else it.getString(it.getColumnIndex(OpenableColumns.DISPLAY_NAME)) } - - @SuppressLint("Range") - val fileNameWithExtension = if (uri.scheme == ContentResolver.SCHEME_CONTENT) { - val cursor = activity.contentResolver.query(uri, null, null, null, null) - cursor?.use { - if (!it.moveToFirst()) null - else it.getString(it.getColumnIndex(OpenableColumns.DISPLAY_NAME)) - } - } else null - - val file = File(path) - val name = file.name - val result = Result.success( - FileMedia( - fileNameWithExtension ?: name, - uri.toString(), - ) + } else null + + val file = File(path) + val name = file.name + val result = Result.success( + FileMedia( + fileNameWithExtension ?: name, + uri.toString(), ) - callback.invoke(result) - } + ) + callback.invoke(result) } - fun pickFile(callback: (Result) -> Unit) { + fun pick(callback: (Result) -> Unit) { this.callback?.let { it.callback.invoke(Result.failure(IllegalStateException("Callback should be null"))) this.callback = null @@ -75,7 +68,7 @@ internal class FilePickerDelegate { this.callback = CallbackData(callback) - filePickerLauncherHolder.value?.launch( + pickerLauncherHolder.value?.launch( arrayOf( "*/*", ) diff --git a/media/src/androidMain/kotlin/dev/icerock/moko/media/picker/GalleryPickerDelegate.kt b/media/src/androidMain/kotlin/dev/icerock/moko/media/picker/GalleryPickerDelegate.kt new file mode 100644 index 0000000..b11a6d4 --- /dev/null +++ b/media/src/androidMain/kotlin/dev/icerock/moko/media/picker/GalleryPickerDelegate.kt @@ -0,0 +1,70 @@ +/* + * Copyright 2024 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.media.picker + +import android.content.Context +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.ActivityResultRegistry +import androidx.activity.result.PickVisualMediaRequest +import androidx.activity.result.contract.ActivityResultContracts + +internal class GalleryPickerDelegate : + ImagePickerDelegate() { + + override fun registerActivityResult( + context: Context, + activityResultRegistry: ActivityResultRegistry + ): ActivityResultLauncher = activityResultRegistry.register( + PICK_GALLERY_IMAGE_KEY, + ActivityResultContracts.PickVisualMedia(), + ) { uri -> + val callbackData = callback ?: return@register + callback = null + + if (uri == null) { + callbackData.callback.invoke(Result.failure(CanceledException())) + return@register + } + + processResult( + context = context, + callback = callbackData.callback, + uri = uri, + maxImageWidth = callbackData.maxWidth, + maxImageHeight = callbackData.maxHeight, + ) + } + + fun pick( + maxWidth: Int, + maxHeight: Int, + callback: (Result) -> Unit, + ) { + this.callback?.let { + it.callback.invoke(Result.failure(IllegalStateException("Callback should be null"))) + this.callback = null + } + + this.callback = CallbackData( + callback, + maxWidth, + maxHeight, + ) + + pickerLauncherHolder.value?.launch( + PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly) + ) + } + + class CallbackData( + val callback: (Result) -> Unit, + val maxWidth: Int, + val maxHeight: Int, + ) + + companion object { + private const val PICK_GALLERY_IMAGE_KEY = "PickGalleryImageKey" + } +} diff --git a/media/src/androidMain/kotlin/dev/icerock/moko/media/picker/ImagePickerDelegate.kt b/media/src/androidMain/kotlin/dev/icerock/moko/media/picker/ImagePickerDelegate.kt index 1532a57..a9bcc59 100644 --- a/media/src/androidMain/kotlin/dev/icerock/moko/media/picker/ImagePickerDelegate.kt +++ b/media/src/androidMain/kotlin/dev/icerock/moko/media/picker/ImagePickerDelegate.kt @@ -1,153 +1,13 @@ -/* - * Copyright 2024 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. - */ - package dev.icerock.moko.media.picker import android.content.Context import android.net.Uri -import androidx.activity.ComponentActivity -import androidx.activity.result.ActivityResultLauncher -import androidx.activity.result.ActivityResultRegistryOwner -import androidx.activity.result.PickVisualMediaRequest -import androidx.activity.result.contract.ActivityResultContracts -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleEventObserver -import androidx.lifecycle.LifecycleOwner import dev.icerock.moko.media.BitmapUtils -import kotlinx.coroutines.flow.MutableStateFlow - -internal class ImagePickerDelegate { - - private var callback: CallbackData? = null - - private val takePictureLauncherHolder = MutableStateFlow?>(null) - private val pickVisualMediaLauncherHolder = - MutableStateFlow?>(null) - - fun bind(activity: ComponentActivity) { - val activityResultRegistryOwner = activity as ActivityResultRegistryOwner - val activityResultRegistry = activityResultRegistryOwner.activityResultRegistry - - pickVisualMediaLauncherHolder.value = activityResultRegistry.register( - PICK_GALLERY_IMAGE_KEY, - ActivityResultContracts.PickVisualMedia(), - ) { uri -> - val callbackData = callback ?: return@register - callback = null - - if (callbackData !is CallbackData.Gallery) { - callbackData.callback.invoke( - Result.failure( - IllegalStateException("Callback type should be Gallery") - ) - ) - return@register - } - - if (uri == null) { - callbackData.callback.invoke(Result.failure(CanceledException())) - return@register - } - - processResult( - context = activity, - callback = callbackData.callback, - uri = uri, - maxImageWidth = callbackData.maxWidth, - maxImageHeight = callbackData.maxHeight, - ) - } - takePictureLauncherHolder.value = activityResultRegistry.register( - "TakePicture", - ActivityResultContracts.TakePicture(), - ) { result -> - val callbackData = callback ?: return@register - callback = null - - if (callbackData !is CallbackData.Camera) { - callbackData.callback.invoke( - Result.failure( - IllegalStateException("Callback type should be Camera") - ) - ) - return@register - } - - if (!result) { - callbackData.callback.invoke(Result.failure(CanceledException())) - return@register - } - - processResult( - context = activity, - callback = callbackData.callback, - uri = callbackData.outputUri, - maxImageWidth = callbackData.maxWidth, - maxImageHeight = callbackData.maxHeight, - ) - } - - val observer = object : LifecycleEventObserver { - - override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { - if (event == Lifecycle.Event.ON_DESTROY) { - takePictureLauncherHolder.value = null - pickVisualMediaLauncherHolder.value = null - source.lifecycle.removeObserver(this) - } - } - } - activity.lifecycle.addObserver(observer) - } - - fun pickGalleryImage( - maxWidth: Int, - maxHeight: Int, - callback: (Result) -> Unit, - ) { - this.callback?.let { - it.callback.invoke(Result.failure(IllegalStateException("Callback should be null"))) - this.callback = null - } - - this.callback = CallbackData.Gallery( - callback, - maxWidth, - maxHeight, - ) - - pickVisualMediaLauncherHolder.value?.launch( - PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly) - ) - } - - fun pickCameraImage( - maxWidth: Int, - maxHeight: Int, - callback: (Result) -> Unit, - outputUri: Uri, - ) { - this.callback?.let { - it.callback.invoke(Result.failure(IllegalStateException("Callback should be null"))) - this.callback = null - } - - this.callback = CallbackData.Camera( - callback, - outputUri, - maxWidth, - maxHeight, - ) - - takePictureLauncherHolder.value?.launch( - outputUri - ) - } +internal abstract class ImagePickerDelegate : PickerDelegate() { @Suppress("ReturnCount") - private fun processResult( + protected fun processResult( context: Context, callback: (Result) -> Unit, uri: Uri, @@ -182,23 +42,4 @@ internal class ImagePickerDelegate { callback.invoke(Result.success(bitmap)) } - - sealed class CallbackData(val callback: (Result) -> Unit) { - class Gallery( - callback: (Result) -> Unit, - val maxWidth: Int, - val maxHeight: Int, - ) : CallbackData(callback) - - class Camera( - callback: (Result) -> Unit, - val outputUri: Uri, - val maxWidth: Int, - val maxHeight: Int, - ) : CallbackData(callback) - } - - companion object { - private const val PICK_GALLERY_IMAGE_KEY = "PickGalleryImageKey" - } } diff --git a/media/src/androidMain/kotlin/dev/icerock/moko/media/picker/MediaPickerControllerImpl.kt b/media/src/androidMain/kotlin/dev/icerock/moko/media/picker/MediaPickerControllerImpl.kt index f94ae7c..a12da53 100755 --- a/media/src/androidMain/kotlin/dev/icerock/moko/media/picker/MediaPickerControllerImpl.kt +++ b/media/src/androidMain/kotlin/dev/icerock/moko/media/picker/MediaPickerControllerImpl.kt @@ -32,7 +32,8 @@ internal class MediaPickerControllerImpl( private var photoFilePath: String? = null - private val imagePickerDelegate = ImagePickerDelegate() + private val galleryPickerDelegate = GalleryPickerDelegate() + private val cameraPickerDelegate = CameraPickerDelegate() private val mediaPickerDelegate = MediaPickerDelegate() private val filePickerDelegate = FilePickerDelegate() @@ -40,7 +41,8 @@ internal class MediaPickerControllerImpl( this.activityHolder.value = activity permissionsController.bind(activity) - imagePickerDelegate.bind(activity) + galleryPickerDelegate.bind(activity) + cameraPickerDelegate.bind(activity) mediaPickerDelegate.bind(activity) filePickerDelegate.bind(activity) @@ -74,16 +76,16 @@ internal class MediaPickerControllerImpl( val outputUri = createPhotoUri() - val bitmap = suspendCoroutine { continuation -> + val bitmap = suspendCoroutine { continuation -> val action: (Result) -> Unit = { continuation.resumeWith(it) } when (source) { - MediaSource.GALLERY -> imagePickerDelegate.pickGalleryImage( + MediaSource.GALLERY -> galleryPickerDelegate.pick( maxWidth, maxHeight, action, ) - MediaSource.CAMERA -> imagePickerDelegate.pickCameraImage( + MediaSource.CAMERA -> cameraPickerDelegate.pick( maxWidth, maxHeight, action, @@ -113,16 +115,16 @@ internal class MediaPickerControllerImpl( return suspendCoroutine { continuation -> val action: (Result) -> Unit = { continuation.resumeWith(it) } - mediaPickerDelegate.pickMedia(action) + mediaPickerDelegate.pick(action) } } override suspend fun pickFiles(): FileMedia { permissionsController.providePermission(Permission.STORAGE) - val path = suspendCoroutine { continuation -> + val path = suspendCoroutine { continuation -> val action: (Result) -> Unit = { continuation.resumeWith(it) } - filePickerDelegate.pickFile(action) + filePickerDelegate.pick(action) } return path diff --git a/media/src/androidMain/kotlin/dev/icerock/moko/media/picker/MediaPickerDelegate.kt b/media/src/androidMain/kotlin/dev/icerock/moko/media/picker/MediaPickerDelegate.kt index 91967cf..a2e15df 100644 --- a/media/src/androidMain/kotlin/dev/icerock/moko/media/picker/MediaPickerDelegate.kt +++ b/media/src/androidMain/kotlin/dev/icerock/moko/media/picker/MediaPickerDelegate.kt @@ -4,69 +4,52 @@ package dev.icerock.moko.media.picker -import androidx.activity.ComponentActivity +import android.content.Context import androidx.activity.result.ActivityResultLauncher -import androidx.activity.result.ActivityResultRegistryOwner +import androidx.activity.result.ActivityResultRegistry import androidx.activity.result.PickVisualMediaRequest import androidx.activity.result.contract.ActivityResultContracts -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleEventObserver -import androidx.lifecycle.LifecycleOwner import dev.icerock.moko.media.Media import dev.icerock.moko.media.MediaFactory import kotlinx.coroutines.flow.MutableStateFlow -internal class MediaPickerDelegate { +internal class MediaPickerDelegate : + PickerDelegate() { - private var callback: CallbackData? = null + override fun registerActivityResult( + context: Context, + activityResultRegistry: ActivityResultRegistry + ): ActivityResultLauncher = activityResultRegistry.register( + PICK_MEDIA_KEY, + ActivityResultContracts.PickVisualMedia(), + ) { uri -> + val callbackData = callback ?: return@register + callback = null - private val mediaPickerLauncherHolder = - MutableStateFlow?>(null) - - fun bind(activity: ComponentActivity) { - val activityResultRegistryOwner = activity as ActivityResultRegistryOwner - val activityResultRegistry = activityResultRegistryOwner.activityResultRegistry - - mediaPickerLauncherHolder.value = activityResultRegistry.register( - PICK_MEDIA_KEY, - ActivityResultContracts.PickVisualMedia() - ) { uri -> - val callbackData = callback ?: return@register - callback = null - - val callback = callbackData.callback + val callback = callbackData.callback - if (uri == null) { - callback.invoke(Result.failure(CanceledException())) - return@register - } - - val result = kotlin.runCatching { - MediaFactory.create(activity, uri) - } - callback.invoke(result) + if (uri == null) { + callback.invoke(Result.failure(CanceledException())) + return@register } - val observer = object : LifecycleEventObserver { - - override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { - if (event == Lifecycle.Event.ON_DESTROY) { - mediaPickerLauncherHolder.value = null - source.lifecycle.removeObserver(this) - } - } + val result = kotlin.runCatching { + MediaFactory.create(context, uri) } - activity.lifecycle.addObserver(observer) + callback.invoke(result) } - fun pickMedia(callback: (Result) -> Unit) { + private val mediaPickerLauncherHolder = + MutableStateFlow?>(null) + + fun pick(callback: (Result) -> Unit) { this.callback?.let { it.callback.invoke(Result.failure(IllegalStateException("Callback should be null"))) this.callback = null } this.callback = CallbackData(callback) - mediaPickerLauncherHolder.value?.launch( + pickerLauncherHolder.value?.launch( PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageAndVideo) ) } diff --git a/media/src/androidMain/kotlin/dev/icerock/moko/media/picker/PickerDelegate.kt b/media/src/androidMain/kotlin/dev/icerock/moko/media/picker/PickerDelegate.kt new file mode 100644 index 0000000..823b189 --- /dev/null +++ b/media/src/androidMain/kotlin/dev/icerock/moko/media/picker/PickerDelegate.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2024 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.media.picker + +import android.content.Context +import androidx.activity.ComponentActivity +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.ActivityResultRegistry +import androidx.activity.result.ActivityResultRegistryOwner +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.LifecycleOwner +import kotlinx.coroutines.flow.MutableStateFlow + +/** + * @param type of callback + * @param type of the input required to launch + */ +internal abstract class PickerDelegate { + + protected var callback: C? = null + + protected val pickerLauncherHolder = + MutableStateFlow?>(null) + + fun bind(activity: ComponentActivity) { + val activityResultRegistryOwner = activity as ActivityResultRegistryOwner + val activityResultRegistry = activityResultRegistryOwner.activityResultRegistry + + pickerLauncherHolder.value = registerActivityResult( + context = activity, + activityResultRegistry = activityResultRegistry, + ) + + val observer = object : LifecycleEventObserver { + override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { + if (event == Lifecycle.Event.ON_DESTROY) { + pickerLauncherHolder.value = null + source.lifecycle.removeObserver(this) + } + } + } + activity.lifecycle.addObserver(observer) + } + + abstract fun registerActivityResult( + context: Context, + activityResultRegistry: ActivityResultRegistry, + ): ActivityResultLauncher +}