From 9f788f9cef8b19d6f3fc83972658b61f6aa5a3f6 Mon Sep 17 00:00:00 2001 From: Alexey Nesterov Date: Fri, 26 Apr 2024 14:02:01 +0700 Subject: [PATCH] #34 add FilePickerDelegate --- .../moko/media/picker/FilePickerDelegate.kt | 88 +++++++++++++++++++ .../media/picker/MediaPickerControllerImpl.kt | 72 +-------------- 2 files changed, 91 insertions(+), 69 deletions(-) create mode 100644 media/src/androidMain/kotlin/dev/icerock/moko/media/picker/FilePickerDelegate.kt 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 new file mode 100644 index 0000000..08842df --- /dev/null +++ b/media/src/androidMain/kotlin/dev/icerock/moko/media/picker/FilePickerDelegate.kt @@ -0,0 +1,88 @@ +package dev.icerock.moko.media.picker + +import android.annotation.SuppressLint +import android.content.ContentResolver +import android.provider.OpenableColumns +import androidx.activity.ComponentActivity +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.ActivityResultRegistryOwner +import androidx.activity.result.contract.ActivityResultContracts +import dev.icerock.moko.media.FileMedia +import kotlinx.coroutines.flow.MutableStateFlow +import java.io.File + +internal class FilePickerDelegate { + + private var callback: CallbackData? = null + + private val filePickerLauncherHolder = + MutableStateFlow>?>(null) + + 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.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 = activity.contentResolver.query(uri, null, null, null, null) + cursor?.use { + if (!it.moveToFirst()) null + else it.getString(it.getColumnIndex(OpenableColumns.DISPLAY_NAME)) + } + } else null + + uri.path?.let { path -> + val file = File(path) + val name = file.name + callback( + Result.success( + FileMedia( + fileNameWithExtension ?: name, + uri.toString(), + ) + ) + ) + } + } + } + + fun pickFile(callback: (Result) -> Unit) { + this.callback?.let { + it.callback.invoke(Result.failure(IllegalStateException("Callback should be null"))) + this.callback = null + } + + this.callback = CallbackData(callback) + + filePickerLauncherHolder.value?.launch( + arrayOf( + "*/*", + ) + ) + } + + class CallbackData(val callback: (Result) -> Unit) + + companion object { + private const val PICK_FILE_KEY = "PickFileKey" + } +} 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 c8fc1d6..f4ea103 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 @@ -8,9 +8,6 @@ import android.app.Activity import android.net.Uri import android.os.Environment import androidx.activity.ComponentActivity -import androidx.activity.result.ActivityResultLauncher -import androidx.activity.result.ActivityResultRegistryOwner -import androidx.activity.result.contract.ActivityResultContracts import androidx.core.content.FileProvider import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleObserver @@ -26,7 +23,6 @@ import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.first import kotlinx.coroutines.withTimeoutOrNull import java.io.File -import java.util.UUID import kotlin.coroutines.suspendCoroutine internal class MediaPickerControllerImpl( @@ -37,14 +33,9 @@ internal class MediaPickerControllerImpl( private var photoFilePath: String? = null - private val key = UUID.randomUUID().toString() - - private val codeCallbackMap = mutableMapOf>() - - private val pickFileMediaLauncherHolder = MutableStateFlow>?>(null) - private val imagePickerDelegate = ImagePickerDelegate() private val mediaPickerDelegate = MediaPickerDelegate() + private val filePickerDelegate = FilePickerDelegate() override fun bind(activity: ComponentActivity) { this.activityHolder.value = activity @@ -52,38 +43,13 @@ internal class MediaPickerControllerImpl( imagePickerDelegate.bind(activity) mediaPickerDelegate.bind(activity) - - val activityResultRegistryOwner = activity as ActivityResultRegistryOwner - - val pickFileMediaLauncher = activityResultRegistryOwner.activityResultRegistry.register( - "PickFileMedia-$key", - ActivityResultContracts.OpenDocument() - ) { uri -> - val callbackData = codeCallbackMap.values.last() as CallbackData - val callback = callbackData.callback - - if (uri != null) { - callback.invoke(Result.failure(CanceledException())) - return@register - } - - if (uri?.path == null) { - callback.invoke(Result.failure(java.lang.IllegalStateException("File is null"))) - return@register - } - uri.path?.let { path -> - // TODO pass result - } - } - - pickFileMediaLauncherHolder.value = pickFileMediaLauncher + filePickerDelegate.bind(activity) val observer = object : LifecycleObserver { @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY) fun onDestroyed(source: LifecycleOwner) { this@MediaPickerControllerImpl.activityHolder.value = null - this@MediaPickerControllerImpl.pickFileMediaLauncherHolder.value = null source.lifecycle.removeObserver(this) } } @@ -149,26 +115,12 @@ internal class MediaPickerControllerImpl( } } - private fun pickFile(callback: (Result) -> Unit) { - val requestCode = codeCallbackMap.keys.sorted().lastOrNull() ?: 0 - codeCallbackMap[requestCode] = - CallbackData.FileMedia( - callback - ) - val launcher = pickFileMediaLauncherHolder.value - launcher?.launch( - arrayOf( - "*/*", - ) - ) - } - override suspend fun pickFiles(): FileMedia { permissionsController.providePermission(Permission.STORAGE) val path = suspendCoroutine { continuation -> val action: (Result) -> Unit = { continuation.resumeWith(it) } - pickFile(action) + filePickerDelegate.pickFile(action) } return path @@ -196,24 +148,6 @@ internal class MediaPickerControllerImpl( } } - sealed class CallbackData(val callback: (Result) -> Unit) { - class Gallery(callback: (Result) -> Unit) : - CallbackData(callback) - - class Camera( - callback: (Result) -> Unit, - val outputUri: Uri - ) : CallbackData(callback) - - class Media( - callback: (Result) -> Unit, - ) : CallbackData(callback) - - class FileMedia( - callback: (Result) -> Unit, - ) : CallbackData(callback) - } - companion object { private const val AWAIT_ACTIVITY_TIMEOUT_DURATION_MS = 2000L private const val DEFAULT_FILE_NAME = "image.png"