Skip to content

Commit

Permalink
icerockdev#34 add FilePickerDelegate
Browse files Browse the repository at this point in the history
  • Loading branch information
Alexey Nesterov committed May 2, 2024
1 parent 39c2649 commit 9f788f9
Show file tree
Hide file tree
Showing 2 changed files with 91 additions and 69 deletions.
Original file line number Diff line number Diff line change
@@ -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<ActivityResultLauncher<Array<String>>?>(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<FileMedia>) -> 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<FileMedia>) -> Unit)

companion object {
private const val PICK_FILE_KEY = "PickFileKey"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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(
Expand All @@ -37,53 +33,23 @@ internal class MediaPickerControllerImpl(

private var photoFilePath: String? = null

private val key = UUID.randomUUID().toString()

private val codeCallbackMap = mutableMapOf<Int, CallbackData<*>>()

private val pickFileMediaLauncherHolder = MutableStateFlow<ActivityResultLauncher<Array<String>>?>(null)

private val imagePickerDelegate = ImagePickerDelegate()
private val mediaPickerDelegate = MediaPickerDelegate()
private val filePickerDelegate = FilePickerDelegate()

override fun bind(activity: ComponentActivity) {
this.activityHolder.value = activity
permissionsController.bind(activity)

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<FileMedia>
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)
}
}
Expand Down Expand Up @@ -149,26 +115,12 @@ internal class MediaPickerControllerImpl(
}
}

private fun pickFile(callback: (Result<FileMedia>) -> 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<FileMedia> { continuation ->
val action: (Result<FileMedia>) -> Unit = { continuation.resumeWith(it) }
pickFile(action)
filePickerDelegate.pickFile(action)
}

return path
Expand Down Expand Up @@ -196,24 +148,6 @@ internal class MediaPickerControllerImpl(
}
}

sealed class CallbackData<T>(val callback: (Result<T>) -> Unit) {
class Gallery(callback: (Result<android.graphics.Bitmap>) -> Unit) :
CallbackData<android.graphics.Bitmap>(callback)

class Camera(
callback: (Result<android.graphics.Bitmap>) -> Unit,
val outputUri: Uri
) : CallbackData<android.graphics.Bitmap>(callback)

class Media(
callback: (Result<dev.icerock.moko.media.Media>) -> Unit,
) : CallbackData<dev.icerock.moko.media.Media>(callback)

class FileMedia(
callback: (Result<dev.icerock.moko.media.FileMedia>) -> Unit,
) : CallbackData<dev.icerock.moko.media.FileMedia>(callback)
}

companion object {
private const val AWAIT_ACTIVITY_TIMEOUT_DURATION_MS = 2000L
private const val DEFAULT_FILE_NAME = "image.png"
Expand Down

0 comments on commit 9f788f9

Please sign in to comment.