From cb4cd58bd7d380e19e4e5a7fcbc3b93c6eb207ca Mon Sep 17 00:00:00 2001 From: Aleksey Mikhailov Date: Mon, 10 Apr 2023 23:36:14 +0600 Subject: [PATCH 1/5] #63 compose multiplatform initial support --- build.gradle.kts | 2 + gradle.properties | 7 ++- gradle/libs.versions.toml | 29 +++++++----- gradle/wrapper/gradle-wrapper.properties | 2 +- media-compose/build.gradle.kts | 31 +++++++++++++ .../compose/BindMediaPickerEffect.android.kt | 28 ++++++++++++ .../moko/media/compose/BitmapExt.android.kt | 13 ++++++ .../MediaPickerControllerFactory.android.kt | 33 ++++++++++++++ .../media/compose/BindMediaPickerEffect.kt | 12 +++++ .../icerock/moko/media/compose/BitmapExt.kt | 10 +++++ .../compose/MediaPickerControllerFactory.kt | 17 +++++++ .../compose/BindMediaPickerEffect.ios.kt | 15 +++++++ .../moko/media/compose/BitmapExt.ios.kt | 14 ++++++ .../MediaPickerControllerFactory.ios.kt | 45 +++++++++++++++++++ settings.gradle.kts | 1 + 15 files changed, 245 insertions(+), 14 deletions(-) create mode 100644 media-compose/build.gradle.kts create mode 100644 media-compose/src/androidMain/kotlin/dev/icerock/moko/media/compose/BindMediaPickerEffect.android.kt create mode 100644 media-compose/src/androidMain/kotlin/dev/icerock/moko/media/compose/BitmapExt.android.kt create mode 100644 media-compose/src/androidMain/kotlin/dev/icerock/moko/media/compose/MediaPickerControllerFactory.android.kt create mode 100644 media-compose/src/commonMain/kotlin/dev/icerock/moko/media/compose/BindMediaPickerEffect.kt create mode 100644 media-compose/src/commonMain/kotlin/dev/icerock/moko/media/compose/BitmapExt.kt create mode 100644 media-compose/src/commonMain/kotlin/dev/icerock/moko/media/compose/MediaPickerControllerFactory.kt create mode 100644 media-compose/src/iosMain/kotlin/dev/icerock/moko/media/compose/BindMediaPickerEffect.ios.kt create mode 100644 media-compose/src/iosMain/kotlin/dev/icerock/moko/media/compose/BitmapExt.ios.kt create mode 100644 media-compose/src/iosMain/kotlin/dev/icerock/moko/media/compose/MediaPickerControllerFactory.ios.kt diff --git a/build.gradle.kts b/build.gradle.kts index 7edb7bd..ad40632 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -14,6 +14,8 @@ buildscript { classpath(libs.androidGradlePlugin) classpath(libs.mokoGradlePlugin) classpath(libs.mobileMultiplatformGradlePlugin) + classpath(libs.composeJetBrainsGradlePlugin) + classpath(libs.detektGradlePlugin) } } diff --git a/gradle.properties b/gradle.properties index 5c80605..dac9384 100755 --- a/gradle.properties +++ b/gradle.properties @@ -3,11 +3,14 @@ org.gradle.configureondemand=false org.gradle.parallel=true kotlin.code.style=official +kotlin.mpp.androidSourceSetLayoutVersion=2 + +org.jetbrains.compose.experimental.uikit.enabled=true android.useAndroidX=true -moko.android.targetSdk=31 -moko.android.compileSdk=31 +moko.android.targetSdk=33 +moko.android.compileSdk=33 moko.android.minSdk=19 moko.publish.name=MOKO media diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 21ce904..752c5ac 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,15 +1,16 @@ [versions] -kotlinVersion = "1.6.21" -androidAppCompatVersion = "1.3.1" -materialDesignVersion = "1.4.0" +kotlinVersion = "1.8.10" +androidAppCompatVersion = "1.6.1" +materialDesignVersion = "1.8.0" androidLifecycleVersion = "2.3.1" -androidExifInterface = "1.3.2" +androidExifInterface = "1.3.6" androidMediaFilePicker = "1.9.1" -coroutinesVersion = "1.6.0-native-mt" -mokoMvvmVersion = "0.13.0" -mokoPermissionsVersion = "0.11.0" +coroutinesVersion = "1.6.4" +mokoMvvmVersion = "0.16.0" +mokoPermissionsVersion = "0.15.0" mokoTestVersion = "0.6.1" -mokoMediaVersion = "0.10.0" +mokoMediaVersion = "0.11.0" +composeJetBrainsVersion = "1.3.1" [libraries] appCompat = { module = "androidx.appcompat:appcompat", version.ref = "androidAppCompatVersion" } @@ -18,14 +19,20 @@ lifecycle = { module = "androidx.lifecycle:lifecycle-extensions", version.ref = exifInterface = { module = "androidx.exifinterface:exifinterface", version.ref = "androidExifInterface" } mediaFilePicker = { module = "com.github.icerockdev:MaterialFilePicker", version.ref = "androidMediaFilePicker" } coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutinesVersion" } + mokoPermissions = { module = "dev.icerock.moko:permissions", version.ref = "mokoPermissionsVersion" } +mokoPermissionsCompose = { module = "dev.icerock.moko:permissions-compose", version.ref = "mokoPermissionsVersion" } + mokoMvvmCore = { module = "dev.icerock.moko:mvvm-core", version.ref = "mokoMvvmVersion" } mokoMvvmLiveData = { module = "dev.icerock.moko:mvvm-livedata", version.ref = "mokoMvvmVersion" } + mokoTest = { module = "dev.icerock.moko:test-core", version.ref = "mokoTestVersion" } mokoMvvmTest = { module = "dev.icerock.moko:mvvm-test", version.ref = "mokoMvvmVersion" } mokoPermissionsTest = { module = "dev.icerock.moko:permissions-test", version.ref = "mokoPermissionsVersion" } kotlinGradlePlugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlinVersion" } -androidGradlePlugin = { module = "com.android.tools.build:gradle", version = "7.2.0" } -mokoGradlePlugin = { module = "dev.icerock.moko:moko-gradle-plugin", version = "0.1.0" } -mobileMultiplatformGradlePlugin = { module = "dev.icerock:mobile-multiplatform", version = "0.14.1" } \ No newline at end of file +androidGradlePlugin = { module = "com.android.tools.build:gradle", version = "7.4.2" } +mokoGradlePlugin = { module = "dev.icerock.moko:moko-gradle-plugin", version = "0.2.0" } +mobileMultiplatformGradlePlugin = { module = "dev.icerock:mobile-multiplatform", version = "0.14.2" } +composeJetBrainsGradlePlugin = { module = "org.jetbrains.compose:compose-gradle-plugin", version.ref = "composeJetBrainsVersion" } +detektGradlePlugin = { module = "io.gitlab.arturbosch.detekt:detekt-gradle-plugin", version = "1.22.0" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 00e33ed..774fae8 100755 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/media-compose/build.gradle.kts b/media-compose/build.gradle.kts new file mode 100644 index 0000000..6a58675 --- /dev/null +++ b/media-compose/build.gradle.kts @@ -0,0 +1,31 @@ +/* + * Copyright 2019 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +plugins { + id("dev.icerock.moko.gradle.multiplatform.mobile") + id("dev.icerock.moko.gradle.publication") + id("dev.icerock.moko.gradle.stub.javadoc") + id("dev.icerock.moko.gradle.detekt") + id("org.jetbrains.compose") +} + +android { + namespace = "dev.icerock.moko.media.compose" + + defaultConfig { + minSdk = 21 + } +} + +dependencies { + commonMainApi(projects.media) + commonMainApi(compose.runtime) + commonMainApi(compose.ui) + commonMainApi(libs.mokoPermissionsCompose) + + androidMainImplementation(libs.appCompat) + + // without this i got Could not find "moko-media/media-compose/build/kotlinTransformedMetadataLibraries/commonMain/org.jetbrains.kotlinx-atomicfu-0.17.3-nativeInterop-8G5yng.klib" + commonMainImplementation("org.jetbrains.kotlinx:atomicfu:0.17.3") +} diff --git a/media-compose/src/androidMain/kotlin/dev/icerock/moko/media/compose/BindMediaPickerEffect.android.kt b/media-compose/src/androidMain/kotlin/dev/icerock/moko/media/compose/BindMediaPickerEffect.android.kt new file mode 100644 index 0000000..5632a42 --- /dev/null +++ b/media-compose/src/androidMain/kotlin/dev/icerock/moko/media/compose/BindMediaPickerEffect.android.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2019 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.media.compose + +import android.content.Context +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.fragment.app.FragmentActivity +import androidx.fragment.app.FragmentManager +import androidx.lifecycle.LifecycleOwner +import dev.icerock.moko.media.picker.MediaPickerController + +@Suppress("FunctionNaming") +@Composable +actual fun BindMediaPickerEffect(mediaPickerController: MediaPickerController) { + val lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current + val context: Context = LocalContext.current + + LaunchedEffect(mediaPickerController, lifecycleOwner, context) { + val fragmentManager: FragmentManager = (context as FragmentActivity).supportFragmentManager + + mediaPickerController.bind(lifecycleOwner.lifecycle, fragmentManager) + } +} diff --git a/media-compose/src/androidMain/kotlin/dev/icerock/moko/media/compose/BitmapExt.android.kt b/media-compose/src/androidMain/kotlin/dev/icerock/moko/media/compose/BitmapExt.android.kt new file mode 100644 index 0000000..cf08b66 --- /dev/null +++ b/media-compose/src/androidMain/kotlin/dev/icerock/moko/media/compose/BitmapExt.android.kt @@ -0,0 +1,13 @@ +/* + * Copyright 2023 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.media.compose + +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.asImageBitmap +import dev.icerock.moko.media.Bitmap + +actual fun Bitmap.toImageBitmap(): ImageBitmap { + return this.platformBitmap.asImageBitmap() +} diff --git a/media-compose/src/androidMain/kotlin/dev/icerock/moko/media/compose/MediaPickerControllerFactory.android.kt b/media-compose/src/androidMain/kotlin/dev/icerock/moko/media/compose/MediaPickerControllerFactory.android.kt new file mode 100644 index 0000000..11694b6 --- /dev/null +++ b/media-compose/src/androidMain/kotlin/dev/icerock/moko/media/compose/MediaPickerControllerFactory.android.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2023 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.media.compose + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import dev.icerock.moko.media.picker.MediaPickerController +import dev.icerock.moko.permissions.PermissionsController +import dev.icerock.moko.permissions.compose.PermissionsControllerFactory +import dev.icerock.moko.permissions.compose.rememberPermissionsControllerFactory + +@Composable +actual fun rememberMediaPickerControllerFactory(): MediaPickerControllerFactory { + val permissionsControllerFactory: PermissionsControllerFactory = + rememberPermissionsControllerFactory() + return remember(permissionsControllerFactory) { + object : MediaPickerControllerFactory { + override fun createMediaPickerController( + permissionsController: PermissionsController + ): MediaPickerController { + return MediaPickerController(permissionsController) + } + + override fun createMediaPickerController(): MediaPickerController { + return MediaPickerController( + permissionsController = permissionsControllerFactory.createPermissionsController() + ) + } + } + } +} diff --git a/media-compose/src/commonMain/kotlin/dev/icerock/moko/media/compose/BindMediaPickerEffect.kt b/media-compose/src/commonMain/kotlin/dev/icerock/moko/media/compose/BindMediaPickerEffect.kt new file mode 100644 index 0000000..6b1ed89 --- /dev/null +++ b/media-compose/src/commonMain/kotlin/dev/icerock/moko/media/compose/BindMediaPickerEffect.kt @@ -0,0 +1,12 @@ +/* + * Copyright 2019 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.media.compose + +import androidx.compose.runtime.Composable +import dev.icerock.moko.media.picker.MediaPickerController + +@Suppress("FunctionNaming") +@Composable +expect fun BindMediaPickerEffect(mediaPickerController: MediaPickerController) diff --git a/media-compose/src/commonMain/kotlin/dev/icerock/moko/media/compose/BitmapExt.kt b/media-compose/src/commonMain/kotlin/dev/icerock/moko/media/compose/BitmapExt.kt new file mode 100644 index 0000000..b0437d7 --- /dev/null +++ b/media-compose/src/commonMain/kotlin/dev/icerock/moko/media/compose/BitmapExt.kt @@ -0,0 +1,10 @@ +/* + * Copyright 2023 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.media.compose + +import androidx.compose.ui.graphics.ImageBitmap +import dev.icerock.moko.media.Bitmap + +expect fun Bitmap.toImageBitmap(): ImageBitmap diff --git a/media-compose/src/commonMain/kotlin/dev/icerock/moko/media/compose/MediaPickerControllerFactory.kt b/media-compose/src/commonMain/kotlin/dev/icerock/moko/media/compose/MediaPickerControllerFactory.kt new file mode 100644 index 0000000..4bbcf9a --- /dev/null +++ b/media-compose/src/commonMain/kotlin/dev/icerock/moko/media/compose/MediaPickerControllerFactory.kt @@ -0,0 +1,17 @@ +/* + * Copyright 2023 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.media.compose + +import androidx.compose.runtime.Composable +import dev.icerock.moko.media.picker.MediaPickerController +import dev.icerock.moko.permissions.PermissionsController + +interface MediaPickerControllerFactory { + fun createMediaPickerController(): MediaPickerController + fun createMediaPickerController(permissionsController: PermissionsController): MediaPickerController +} + +@Composable +expect fun rememberMediaPickerControllerFactory(): MediaPickerControllerFactory diff --git a/media-compose/src/iosMain/kotlin/dev/icerock/moko/media/compose/BindMediaPickerEffect.ios.kt b/media-compose/src/iosMain/kotlin/dev/icerock/moko/media/compose/BindMediaPickerEffect.ios.kt new file mode 100644 index 0000000..b3cab90 --- /dev/null +++ b/media-compose/src/iosMain/kotlin/dev/icerock/moko/media/compose/BindMediaPickerEffect.ios.kt @@ -0,0 +1,15 @@ +/* + * Copyright 2019 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.media.compose + +import androidx.compose.runtime.Composable +import dev.icerock.moko.media.picker.MediaPickerController +import dev.icerock.moko.permissions.compose.BindEffect + +@Suppress("FunctionNaming") +@Composable +actual fun BindMediaPickerEffect(mediaPickerController: MediaPickerController) { + BindEffect(mediaPickerController.permissionsController) +} diff --git a/media-compose/src/iosMain/kotlin/dev/icerock/moko/media/compose/BitmapExt.ios.kt b/media-compose/src/iosMain/kotlin/dev/icerock/moko/media/compose/BitmapExt.ios.kt new file mode 100644 index 0000000..b15ec26 --- /dev/null +++ b/media-compose/src/iosMain/kotlin/dev/icerock/moko/media/compose/BitmapExt.ios.kt @@ -0,0 +1,14 @@ +/* + * Copyright 2023 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.media.compose + +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.toComposeImageBitmap +import dev.icerock.moko.media.Bitmap +import org.jetbrains.skia.Image + +actual fun Bitmap.toImageBitmap(): ImageBitmap { + return Image.makeFromEncoded(this.toByteArray()).toComposeImageBitmap() +} diff --git a/media-compose/src/iosMain/kotlin/dev/icerock/moko/media/compose/MediaPickerControllerFactory.ios.kt b/media-compose/src/iosMain/kotlin/dev/icerock/moko/media/compose/MediaPickerControllerFactory.ios.kt new file mode 100644 index 0000000..f8a59ad --- /dev/null +++ b/media-compose/src/iosMain/kotlin/dev/icerock/moko/media/compose/MediaPickerControllerFactory.ios.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2023 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.media.compose + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import dev.icerock.moko.media.picker.ios.MediaPickerController +import dev.icerock.moko.permissions.PermissionsController +import dev.icerock.moko.permissions.compose.PermissionsControllerFactory +import dev.icerock.moko.permissions.compose.rememberPermissionsControllerFactory +import platform.UIKit.UIApplication +import platform.UIKit.UIViewController + +@Composable +actual fun rememberMediaPickerControllerFactory(): MediaPickerControllerFactory { + val permissionsControllerFactory: PermissionsControllerFactory = + rememberPermissionsControllerFactory() + return remember(permissionsControllerFactory) { + val getVC: () -> UIViewController = { + // for compose app in common case used one ComposeViewController in one window. so it will works + UIApplication.sharedApplication.keyWindow?.rootViewController + ?: error("can't find view controller") + } + + object : MediaPickerControllerFactory { + override fun createMediaPickerController( + permissionsController: PermissionsController + ): MediaPickerController { + return MediaPickerController( + permissionsController = permissionsController, + getViewController = getVC + ) + } + + override fun createMediaPickerController(): MediaPickerController { + return MediaPickerController( + permissionsController = permissionsControllerFactory.createPermissionsController(), + getViewController = getVC + ) + } + } + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index aaea879..9b971e9 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -15,6 +15,7 @@ dependencyResolutionManagement { } include(":media") +include(":media-compose") include(":media-test") include(":sample:android-app") include(":sample:mpp-library") From f56c2aec4214d683d5a169be7eed9090261fc126 Mon Sep 17 00:00:00 2001 From: Aleksey Mikhailov Date: Tue, 11 Apr 2023 15:51:49 +0600 Subject: [PATCH 2/5] #63 fix ios picker on new kotlin memory model --- .../media/picker/ios/MediaPickerController.kt | 28 ++++++++----------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/media/src/iosMain/kotlin/dev/icerock/moko/media/picker/ios/MediaPickerController.kt b/media/src/iosMain/kotlin/dev/icerock/moko/media/picker/ios/MediaPickerController.kt index 22f5111..f113234 100644 --- a/media/src/iosMain/kotlin/dev/icerock/moko/media/picker/ios/MediaPickerController.kt +++ b/media/src/iosMain/kotlin/dev/icerock/moko/media/picker/ios/MediaPickerController.kt @@ -32,6 +32,7 @@ class MediaPickerController( override val permissionsController: PermissionsController, private val getViewController: () -> UIViewController ) : MediaPickerControllerProtocol { + private val strongRefs: MutableSet = mutableSetOf() @Suppress("unused") constructor( @@ -51,31 +52,26 @@ class MediaPickerController( permissionsController.providePermission(permission) } - @Suppress("ASSIGNED_BUT_NEVER_ACCESSED_VARIABLE") - var delegatePtr: ImagePickerDelegateToContinuation? // strong reference to delegate (view controller have weak ref) - - @Suppress("ASSIGNED_BUT_NEVER_ACCESSED_VARIABLE") - var presentationDelegate: AdaptivePresentationDelegateToContinuation? // strong reference too - val media = suspendCoroutine { continuation -> - val localDelegatePtr = ImagePickerDelegateToContinuation(continuation) - delegatePtr = localDelegatePtr - val localPresentationDelegatePtr = - AdaptivePresentationDelegateToContinuation(continuation) - presentationDelegate = localPresentationDelegatePtr - + val refs: MutableSet = mutableSetOf() + strongRefs.add(refs) + val media: Media = suspendCoroutine { continuation -> val controller = UIImagePickerController() controller.sourceType = source.toSourceType() controller.mediaTypes = listOf(kImageType) - controller.delegate = localDelegatePtr + controller.delegate = ImagePickerDelegateToContinuation(continuation).also { + refs.add(it) + } getViewController().presentViewController( controller, animated = true, completion = null ) - controller.presentationController?.delegate = localPresentationDelegatePtr + controller.presentationController?.delegate = + AdaptivePresentationDelegateToContinuation(continuation).also { + refs.add(it) + } } - delegatePtr = null - presentationDelegate = null + strongRefs.remove(refs) return media.preview } From 87b752985ae41a7c25c61397cc416e1a8f9cd5e8 Mon Sep 17 00:00:00 2001 From: Aleksey Mikhailov Date: Tue, 11 Apr 2023 15:56:06 +0600 Subject: [PATCH 3/5] fix other strong refs issues --- .../media/picker/ios/MediaPickerController.kt | 40 +++++++++---------- 1 file changed, 18 insertions(+), 22 deletions(-) diff --git a/media/src/iosMain/kotlin/dev/icerock/moko/media/picker/ios/MediaPickerController.kt b/media/src/iosMain/kotlin/dev/icerock/moko/media/picker/ios/MediaPickerController.kt index f113234..2f87140 100644 --- a/media/src/iosMain/kotlin/dev/icerock/moko/media/picker/ios/MediaPickerController.kt +++ b/media/src/iosMain/kotlin/dev/icerock/moko/media/picker/ios/MediaPickerController.kt @@ -76,32 +76,28 @@ class MediaPickerController( } override suspend fun pickFiles(): FileMedia { - @Suppress("ASSIGNED_BUT_NEVER_ACCESSED_VARIABLE") - var delegatePtr: DocumentPickerDelegateToContinuation? // strong reference to delegate (view controller have weak ref) - - @Suppress("ASSIGNED_BUT_NEVER_ACCESSED_VARIABLE") - var presentationDelegate: AdaptivePresentationDelegateToContinuation? // strong reference too - val fileMedia = suspendCoroutine { continuation -> - val localDelegatePtr = DocumentPickerDelegateToContinuation(continuation) - delegatePtr = localDelegatePtr - val localPresentationDelegatePtr = - AdaptivePresentationDelegateToContinuation(continuation) - presentationDelegate = localPresentationDelegatePtr + val refs: MutableSet = mutableSetOf() + strongRefs.add(refs) + val fileMedia: FileMedia = suspendCoroutine { continuation -> val controller = UIDocumentPickerViewController( documentTypes = listOf(kStandardFileTypesId), inMode = UIDocumentPickerMode.UIDocumentPickerModeImport ) - controller.delegate = localDelegatePtr + controller.delegate = DocumentPickerDelegateToContinuation(continuation).also { + refs.add(it) + } getViewController().presentViewController( controller, animated = true, completion = null ) - controller.presentationController?.delegate = localPresentationDelegatePtr + controller.presentationController?.delegate = + AdaptivePresentationDelegateToContinuation(continuation).also { + refs.add(it) + } } - delegatePtr = null - presentationDelegate = null + strongRefs.remove(refs) return fileMedia } @@ -120,24 +116,24 @@ class MediaPickerController( override suspend fun pickMedia(): Media { permissionsController.providePermission(Permission.GALLERY) - @Suppress("ASSIGNED_BUT_NEVER_ACCESSED_VARIABLE") - var delegatePtr: ImagePickerDelegateToContinuation? // strong reference to delegate (view controller have weak ref) - val media = suspendCoroutine { continuation -> - val localDelegatePtr = ImagePickerDelegateToContinuation(continuation) - delegatePtr = localDelegatePtr + val refs: MutableSet = mutableSetOf() + strongRefs.add(refs) + val media: Media = suspendCoroutine { continuation -> val controller = UIImagePickerController() controller.sourceType = UIImagePickerControllerSourceType.UIImagePickerControllerSourceTypePhotoLibrary controller.mediaTypes = listOf(kImageType, kVideoType, kMovieType) - controller.delegate = localDelegatePtr + controller.delegate = ImagePickerDelegateToContinuation(continuation).also { + refs.add(it) + } getViewController().presentViewController( controller, animated = true, completion = null ) } - delegatePtr = null + strongRefs.remove(refs) return media } From fd272d417409964fc2a87bc8e0867cc557309ad5 Mon Sep 17 00:00:00 2001 From: Aleksey Mikhailov Date: Tue, 11 Apr 2023 18:22:21 +0600 Subject: [PATCH 4/5] #44 add bind for iOS --- .../media/test/MediaPickerControllerMock.kt | 3 +++ .../media/picker/ios/MediaPickerController.kt | 27 +++++++++++++++---- .../ios/MediaPickerControllerProtocol.kt | 10 ++----- 3 files changed, 27 insertions(+), 13 deletions(-) diff --git a/media-test/src/iosMain/kotlin/dev/icerock/moko/media/test/MediaPickerControllerMock.kt b/media-test/src/iosMain/kotlin/dev/icerock/moko/media/test/MediaPickerControllerMock.kt index aab215e..34bc091 100644 --- a/media-test/src/iosMain/kotlin/dev/icerock/moko/media/test/MediaPickerControllerMock.kt +++ b/media-test/src/iosMain/kotlin/dev/icerock/moko/media/test/MediaPickerControllerMock.kt @@ -10,10 +10,13 @@ import dev.icerock.moko.media.Media import dev.icerock.moko.media.picker.MediaPickerController import dev.icerock.moko.media.picker.MediaSource import dev.icerock.moko.permissions.PermissionsController +import platform.UIKit.UIViewController actual open class MediaPickerControllerMock actual constructor( actual override val permissionsController: PermissionsController ) : MediaPickerController { + override fun bind(viewController: UIViewController) = Unit + actual override suspend fun pickImage(source: MediaSource): Bitmap { TODO("Not yet implemented") } diff --git a/media/src/iosMain/kotlin/dev/icerock/moko/media/picker/ios/MediaPickerController.kt b/media/src/iosMain/kotlin/dev/icerock/moko/media/picker/ios/MediaPickerController.kt index 2f87140..a739899 100644 --- a/media/src/iosMain/kotlin/dev/icerock/moko/media/picker/ios/MediaPickerController.kt +++ b/media/src/iosMain/kotlin/dev/icerock/moko/media/picker/ios/MediaPickerController.kt @@ -27,21 +27,38 @@ import platform.UIKit.UIImagePickerControllerSourceType import platform.UIKit.UIViewController import platform.UIKit.presentationController import kotlin.coroutines.suspendCoroutine +import kotlin.native.ref.WeakReference class MediaPickerController( - override val permissionsController: PermissionsController, - private val getViewController: () -> UIViewController + override val permissionsController: PermissionsController ) : MediaPickerControllerProtocol { private val strongRefs: MutableSet = mutableSetOf() + private lateinit var getViewController: () -> UIViewController @Suppress("unused") constructor( permissionsController: PermissionsController, viewController: UIViewController ) : this( - permissionsController = permissionsController, - getViewController = { viewController } - ) + permissionsController = permissionsController + ) { + bind(viewController) + } + + @Suppress("unused") + constructor( + permissionsController: PermissionsController, + getViewController: () -> UIViewController + ) : this( + permissionsController = permissionsController + ) { + this.getViewController = getViewController + } + + override fun bind(viewController: UIViewController) { + val weakRef: WeakReference = WeakReference(viewController) + this.getViewController = { weakRef.get() ?: error("viewController was deallocated") } + } override suspend fun pickImage(source: MediaSource): Bitmap { return pickImage(source, DEFAULT_MAX_IMAGE_WIDTH, DEFAULT_MAX_IMAGE_HEIGHT) diff --git a/media/src/iosMain/kotlin/dev/icerock/moko/media/picker/ios/MediaPickerControllerProtocol.kt b/media/src/iosMain/kotlin/dev/icerock/moko/media/picker/ios/MediaPickerControllerProtocol.kt index c33e70f..a6845be 100755 --- a/media/src/iosMain/kotlin/dev/icerock/moko/media/picker/ios/MediaPickerControllerProtocol.kt +++ b/media/src/iosMain/kotlin/dev/icerock/moko/media/picker/ios/MediaPickerControllerProtocol.kt @@ -8,20 +8,14 @@ import dev.icerock.moko.media.Bitmap import dev.icerock.moko.media.FileMedia import dev.icerock.moko.media.Media import dev.icerock.moko.media.picker.MediaSource -import dev.icerock.moko.permissions.Permission import dev.icerock.moko.permissions.PermissionsController -import platform.CoreServices.kUTTypeImage -import platform.CoreServices.kUTTypeMovie -import platform.CoreServices.kUTTypeVideo -import platform.Foundation.CFBridgingRelease -import platform.UIKit.UIImagePickerController -import platform.UIKit.UIImagePickerControllerSourceType import platform.UIKit.UIViewController -import kotlin.coroutines.suspendCoroutine interface MediaPickerControllerProtocol { val permissionsController: PermissionsController + fun bind(viewController: UIViewController) + suspend fun pickImage(source: MediaSource): Bitmap suspend fun pickImage(source: MediaSource, maxWidth: Int, maxHeight: Int): Bitmap suspend fun pickMedia(): Media From ced5cfabcccd7e734f7b51b1cf17cd468e377ad7 Mon Sep 17 00:00:00 2001 From: Aleksey Mikhailov Date: Tue, 11 Apr 2023 18:33:21 +0600 Subject: [PATCH 5/5] #44 detekt fixes --- .../kotlin/dev/icerock/moko/media/MediaFactory.kt | 6 +++--- .../moko/media/picker/MediaPickerControllerImpl.kt | 6 +++--- .../iosMain/kotlin/dev/icerock/moko/media/Bitmap.kt | 10 +++++++--- .../kotlin/dev/icerock/moko/media/FileMediaExt.kt | 2 +- .../media/picker/ImagePickerDelegateToContinuation.kt | 4 ++-- .../icerock/moko/media/player/MediaPlayerController.kt | 10 ++++++++-- 6 files changed, 24 insertions(+), 14 deletions(-) diff --git a/media/src/androidMain/kotlin/dev/icerock/moko/media/MediaFactory.kt b/media/src/androidMain/kotlin/dev/icerock/moko/media/MediaFactory.kt index ecb06e0..be96e75 100755 --- a/media/src/androidMain/kotlin/dev/icerock/moko/media/MediaFactory.kt +++ b/media/src/androidMain/kotlin/dev/icerock/moko/media/MediaFactory.kt @@ -38,7 +38,7 @@ object MediaFactory { return cursorRef.use { cursor -> if (!cursor.moveToFirst()) { - throw IllegalStateException("cursor should have one element") + error("cursor should have one element") } val mimeTypeIndex = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.MIME_TYPE) @@ -68,7 +68,7 @@ object MediaFactory { return cursorRef.use { cursor -> if (!cursor.moveToFirst()) { - throw IllegalStateException("not found resource") + error("not found resource") } val orientation = contentResolver.openInputStream(uri)?.use { @@ -107,7 +107,7 @@ object MediaFactory { return cursorRef.use { cursor -> if (!cursor.moveToFirst()) { - throw IllegalStateException("cursor should have one element") + error("cursor should have one element") } val titleColumn = cursor.getColumnIndex(MediaStore.Video.VideoColumns.TITLE) 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 1c52c12..8eda81e 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 @@ -51,7 +51,7 @@ internal class MediaPickerControllerImpl( */ override suspend fun pickImage(source: MediaSource, maxWidth: Int, maxHeight: Int): Bitmap { val fragmentManager = - fragmentManager ?: throw IllegalStateException("can't pick image without active window") + fragmentManager ?: error("can't pick image without active window") source.requiredPermissions().forEach { permission -> permissionsController.providePermission(permission) @@ -82,7 +82,7 @@ internal class MediaPickerControllerImpl( override suspend fun pickMedia(): Media { val fragmentManager = - fragmentManager ?: throw IllegalStateException("can't pick image without active window") + fragmentManager ?: error("can't pick image without active window") permissionsController.providePermission(Permission.GALLERY) @@ -106,7 +106,7 @@ internal class MediaPickerControllerImpl( override suspend fun pickFiles(): FileMedia { val fragmentManager = - fragmentManager ?: throw IllegalStateException("can't pick image without active window") + fragmentManager ?: error("can't pick image without active window") permissionsController.providePermission(Permission.STORAGE) diff --git a/media/src/iosMain/kotlin/dev/icerock/moko/media/Bitmap.kt b/media/src/iosMain/kotlin/dev/icerock/moko/media/Bitmap.kt index 02308a4..e575ae4 100755 --- a/media/src/iosMain/kotlin/dev/icerock/moko/media/Bitmap.kt +++ b/media/src/iosMain/kotlin/dev/icerock/moko/media/Bitmap.kt @@ -21,7 +21,7 @@ import platform.UIKit.UIImageJPEGRepresentation actual class Bitmap(val image: UIImage) { actual fun toByteArray(): ByteArray { - val imageData = UIImageJPEGRepresentation(image, 0.99) + val imageData = UIImageJPEGRepresentation(image, COMPRESSION_QUALITY) ?: throw IllegalArgumentException("image data is null") val bytes = imageData.bytes ?: throw IllegalArgumentException("image bytes is null") val length = imageData.length @@ -31,7 +31,7 @@ actual class Bitmap(val image: UIImage) { } actual fun toBase64(): String { - val imageData = UIImageJPEGRepresentation(image, 0.99) + val imageData = UIImageJPEGRepresentation(image, COMPRESSION_QUALITY) ?: throw IllegalArgumentException("image data is null") return imageData.base64EncodedStringWithOptions(0) @@ -51,9 +51,13 @@ actual class Bitmap(val image: UIImage) { val newImage = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext(); - val imageData = UIImageJPEGRepresentation(newImage!!, 0.99) + val imageData = UIImageJPEGRepresentation(newImage!!, COMPRESSION_QUALITY) ?: throw IllegalArgumentException("image data is null") return imageData.base64EncodedStringWithOptions(0) } + + private companion object { + const val COMPRESSION_QUALITY = 0.99 + } } diff --git a/media/src/iosMain/kotlin/dev/icerock/moko/media/FileMediaExt.kt b/media/src/iosMain/kotlin/dev/icerock/moko/media/FileMediaExt.kt index 194a88a..0df3f7c 100644 --- a/media/src/iosMain/kotlin/dev/icerock/moko/media/FileMediaExt.kt +++ b/media/src/iosMain/kotlin/dev/icerock/moko/media/FileMediaExt.kt @@ -25,4 +25,4 @@ actual fun FileMedia.toByteArray(): ByteArray { val bytesPointer: CPointer = bytes.reinterpret() return ByteArray(length.toInt()) { index -> bytesPointer[index] } -} \ No newline at end of file +} diff --git a/media/src/iosMain/kotlin/dev/icerock/moko/media/picker/ImagePickerDelegateToContinuation.kt b/media/src/iosMain/kotlin/dev/icerock/moko/media/picker/ImagePickerDelegateToContinuation.kt index 4a82cc3..e3ee900 100644 --- a/media/src/iosMain/kotlin/dev/icerock/moko/media/picker/ImagePickerDelegateToContinuation.kt +++ b/media/src/iosMain/kotlin/dev/icerock/moko/media/picker/ImagePickerDelegateToContinuation.kt @@ -35,6 +35,7 @@ internal class ImagePickerDelegateToContinuation constructor( continuation.resumeWith(Result.failure(CanceledException())) } + @Suppress("ReturnCount") override fun imagePickerController( picker: UIImagePickerController, didFinishPickingMediaWithInfo: Map @@ -50,7 +51,6 @@ internal class ImagePickerDelegateToContinuation constructor( picker.dismissViewControllerAnimated(true) {} - //TODO: Строковые константы могут отличаться в разных версиях. Хардкодить их нехорошо, CFBridgingRelease(kUTTypeVideo) возвращает неверный тип. val type = when (mediaType) { MediaPickerController.kMovieType, MediaPickerController.kVideoType -> MediaType.VIDEO MediaPickerController.kImageType -> MediaType.PHOTO @@ -90,7 +90,7 @@ internal class ImagePickerDelegateToContinuation constructor( } } - //TODO: Стоит сделать асинхронно и придумать что делать с ошибкой + // Стоит сделать асинхронно и придумать что делать с ошибкой private fun fetchThumbnail(videoAsset: AVAsset): UIImage { val imageGenerator = AVAssetImageGenerator( asset = videoAsset diff --git a/media/src/iosMain/kotlin/dev/icerock/moko/media/player/MediaPlayerController.kt b/media/src/iosMain/kotlin/dev/icerock/moko/media/player/MediaPlayerController.kt index e19d31e..817cfc3 100755 --- a/media/src/iosMain/kotlin/dev/icerock/moko/media/player/MediaPlayerController.kt +++ b/media/src/iosMain/kotlin/dev/icerock/moko/media/player/MediaPlayerController.kt @@ -5,7 +5,13 @@ package dev.icerock.moko.media.player import kotlinx.cinterop.cValue -import platform.AVFoundation.* +import platform.AVFoundation.AVPlayer +import platform.AVFoundation.AVPlayerItemDidPlayToEndTimeNotification +import platform.AVFoundation.AVPlayerTimeControlStatusPlaying +import platform.AVFoundation.pause +import platform.AVFoundation.play +import platform.AVFoundation.seekToTime +import platform.AVFoundation.timeControlStatus import platform.AVKit.AVPlayerViewController import platform.Foundation.NSNotificationCenter import platform.Foundation.NSOperationQueue @@ -82,4 +88,4 @@ actual class MediaPlayerController { actual fun release() { observer?.let { NSNotificationCenter.defaultCenter.removeObserver(it) } } -} \ No newline at end of file +}