Skip to content

Commit

Permalink
icerockdev#34 add ImagePickerDelegate
Browse files Browse the repository at this point in the history
  • Loading branch information
Alexey Nesterov committed May 2, 2024
1 parent c98aa71 commit d358b7f
Show file tree
Hide file tree
Showing 3 changed files with 188 additions and 61 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
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<ActivityResultLauncher<Uri>?>(null)
private val pickVisualMediaLauncherHolder =
MutableStateFlow<ActivityResultLauncher<PickVisualMediaRequest>?>(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

val callback = callbackData.callback

if (uri == null) {
callback.invoke(Result.failure(CanceledException()))
return@register
}

processResult(activity, callback, uri)
}

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(activity, callbackData.callback, callbackData.outputUri)
}

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<android.graphics.Bitmap>) -> Unit,
) {
this.callback?.let {
it.callback.invoke(Result.failure(IllegalStateException("Callback should be null")))
this.callback = null
}

this.callback = CallbackData.Gallery(callback)

pickVisualMediaLauncherHolder.value?.launch(
PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)
)
}

fun pickCameraImage(
maxWidth: Int,
maxHeight: Int,
callback: (Result<android.graphics.Bitmap>) -> 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)

takePictureLauncherHolder.value?.launch(
outputUri
)
}

@Suppress("ReturnCount")
private fun processResult(
context: Context,
callback: (Result<android.graphics.Bitmap>) -> Unit,
uri: Uri,
maxImageWidth: Int = DEFAULT_MAX_IMAGE_WIDTH,
maxImageHeight: Int = DEFAULT_MAX_IMAGE_HEIGHT,
) {
val contentResolver = context.contentResolver

val bitmapOptions = contentResolver.openInputStream(uri)?.use {
BitmapUtils.getBitmapOptionsFromStream(it)
} ?: run {
callback.invoke(Result.failure(NoAccessToFileException(uri.toString())))
return
}

val sampleSize =
BitmapUtils.calculateInSampleSize(bitmapOptions, maxImageWidth, maxImageHeight)

val orientation = contentResolver.openInputStream(uri)?.use {
BitmapUtils.getBitmapOrientation(it)
} ?: run {
callback.invoke(Result.failure(NoAccessToFileException(uri.toString())))
return
}

val bitmap = contentResolver.openInputStream(uri)?.use {
BitmapUtils.getNormalizedBitmap(it, orientation, sampleSize)
} ?: run {
callback.invoke(Result.failure(NoAccessToFileException(uri.toString())))
return
}

callback.invoke(Result.success(bitmap))
}

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

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

companion object {
private const val PICK_GALLERY_IMAGE_KEY = "PickGalleryImageKey"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,11 @@ internal class MediaPickerControllerImpl(

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

private val takePictureLauncherHolder = MutableStateFlow<ActivityResultLauncher<Uri>?>(null)
private val pickVisualMediaLauncherHolder = MutableStateFlow<ActivityResultLauncher<PickVisualMediaRequest>?>(null)
private val pickFileMediaLauncherHolder = MutableStateFlow<ActivityResultLauncher<Array<String>>?>(null)

private val imagePickerDelegate = ImagePickerDelegate()

private val maxImageWidth
get() = DEFAULT_MAX_IMAGE_WIDTH
private val maxImageHeight
Expand All @@ -57,25 +58,9 @@ internal class MediaPickerControllerImpl(
this.activityHolder.value = activity
permissionsController.bind(activity)

val activityResultRegistryOwner = activity as ActivityResultRegistryOwner
imagePickerDelegate.bind(activity)

val takePictureLauncher = activityResultRegistryOwner.activityResultRegistry.register(
"TakePicture-$key",
ActivityResultContracts.TakePicture()
) { success ->
val callbackData = codeCallbackMap.values.last() as CallbackData<android.graphics.Bitmap>
val callback = callbackData.callback
if (success) {
when (callbackData) {
is CallbackData.Camera -> {
processResult(activity, callback, callbackData.outputUri)
}
else -> Unit
}
} else {
callback.invoke(Result.failure(CanceledException()))
}
}
val activityResultRegistryOwner = activity as ActivityResultRegistryOwner

val pickVisualMediaLauncher = activityResultRegistryOwner.activityResultRegistry.register(
"PickVisualMedia-$key",
Expand Down Expand Up @@ -111,7 +96,6 @@ internal class MediaPickerControllerImpl(
}
}

takePictureLauncherHolder.value = takePictureLauncher
pickVisualMediaLauncherHolder.value = pickVisualMediaLauncher
pickFileMediaLauncherHolder.value = pickFileMediaLauncher

Expand All @@ -120,7 +104,6 @@ internal class MediaPickerControllerImpl(
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
fun onDestroyed(source: LifecycleOwner) {
this@MediaPickerControllerImpl.activityHolder.value = null
this@MediaPickerControllerImpl.takePictureLauncherHolder.value = null
this@MediaPickerControllerImpl.pickVisualMediaLauncherHolder.value = null
this@MediaPickerControllerImpl.pickFileMediaLauncherHolder.value = null
source.lifecycle.removeObserver(this)
Expand Down Expand Up @@ -148,36 +131,24 @@ internal class MediaPickerControllerImpl(
val bitmap = suspendCoroutine<android.graphics.Bitmap> { continuation ->
val action: (Result<android.graphics.Bitmap>) -> Unit = { continuation.resumeWith(it) }
when (source) {
MediaSource.GALLERY -> pickGalleryImage(action)
MediaSource.CAMERA -> pickCameraImage(outputUri, action)
MediaSource.GALLERY -> imagePickerDelegate.pickGalleryImage(
maxWidth,
maxHeight,
action,
)

MediaSource.CAMERA -> imagePickerDelegate.pickCameraImage(
maxWidth,
maxHeight,
action,
outputUri,
)
}
}

return Bitmap(bitmap)
}

private fun pickGalleryImage(callback: (Result<android.graphics.Bitmap>) -> Unit) {
val requestCode = codeCallbackMap.keys.sorted().lastOrNull() ?: 0
codeCallbackMap[requestCode] =
CallbackData.Gallery(
callback
)
val launcher = pickVisualMediaLauncherHolder.value
launcher?.launch(PickVisualMediaRequest())
}

private fun pickCameraImage(outputUri: Uri, callback: (Result<android.graphics.Bitmap>) -> Unit) {
val requestCode = codeCallbackMap.keys.sorted().lastOrNull() ?: 0
codeCallbackMap[requestCode] =
CallbackData.Camera(
callback,
outputUri
)

val launcher = takePictureLauncherHolder.value
launcher?.launch(outputUri)
}

private fun pickMediaFile(callback: (Result<Media>) -> Unit) {
val requestCode = codeCallbackMap.keys.sorted().lastOrNull() ?: 0
codeCallbackMap[requestCode] =
Expand Down Expand Up @@ -219,22 +190,7 @@ internal class MediaPickerControllerImpl(
val launcher = pickFileMediaLauncherHolder.value
launcher?.launch(
arrayOf(
"application/pdf",
"application/octet-stream",
"application/doc",
"application/msword",
"application/ms-doc",
"application/vnd.ms-excel",
"application/vnd.ms-powerpoint",
"application/json",
"application/zip",
"text/plain",
"text/html",
"text/xml",
"audio/mpeg",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
"*/*",
)
)
}
Expand Down
4 changes: 4 additions & 0 deletions sample/android-app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO"/>
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO"/>

<application
android:label="moko-media test app"
Expand Down

0 comments on commit d358b7f

Please sign in to comment.