diff --git a/src/androidMain/kotlin/com/outsidesource/oskitkmp/capability/KmpCapabilities.android.kt b/src/androidMain/kotlin/com/outsidesource/oskitkmp/capability/KmpCapabilities.android.kt index 4f9f3665..aa933941 100644 --- a/src/androidMain/kotlin/com/outsidesource/oskitkmp/capability/KmpCapabilities.android.kt +++ b/src/androidMain/kotlin/com/outsidesource/oskitkmp/capability/KmpCapabilities.android.kt @@ -1,10 +1,9 @@ package com.outsidesource.oskitkmp.capability -import android.content.Intent -import android.net.Uri -import android.provider.Settings import androidx.activity.ComponentActivity import com.outsidesource.oskitkmp.outcome.Outcome +import com.outsidesource.oskitkmp.systemui.KmpSettingsScreenOpener +import com.outsidesource.oskitkmp.systemui.SettingsScreenType actual class KmpCapabilityContext( var activity: ComponentActivity, @@ -17,13 +16,12 @@ internal actual fun createPlatformLocationCapability(flags: Array { - try { - val activity = context?.activity ?: return Outcome.Error(KmpCapabilitiesError.Uninitialized) - val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { - data = Uri.fromParts("package", activity.packageName, null) + return try { + if (context != null) { + KmpSettingsScreenOpener.open(context, SettingsScreenType.App) + } else { + Outcome.Error(KmpCapabilitiesError.Uninitialized) } - activity.startActivity(intent) - return Outcome.Ok(Unit) } catch (e: Exception) { return Outcome.Error(e) } diff --git a/src/androidMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreen.android.kt b/src/androidMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreen.android.kt new file mode 100644 index 00000000..7faacfb9 --- /dev/null +++ b/src/androidMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreen.android.kt @@ -0,0 +1,45 @@ +package com.outsidesource.oskitkmp.systemui + +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.provider.Settings +import com.outsidesource.oskitkmp.capability.KmpCapabilityContext +import com.outsidesource.oskitkmp.outcome.Outcome + +actual object KmpSettingsScreenOpener : IKmpSettingsScreenOpener { + actual override suspend fun open( + context: KmpCapabilityContext, + type: SettingsScreenType, + fallbackToAppSettings: Boolean, + ): Outcome { + val appSettingsIntent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { + data = Uri.fromParts("package", context.activity.packageName, null) + } + + val res = launchIntent( + context.activity, + when (type) { + SettingsScreenType.App -> appSettingsIntent + SettingsScreenType.SystemSettings -> Intent(Settings.ACTION_SETTINGS) + SettingsScreenType.Bluetooth -> Intent(Settings.ACTION_BLUETOOTH_SETTINGS) + SettingsScreenType.Location -> Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS) + }, + ) + + return if (res is Outcome.Error && type != SettingsScreenType.App && fallbackToAppSettings) { + launchIntent(context.activity, appSettingsIntent) + } else { + res + } + } + + private fun launchIntent(context: Context, intent: Intent): Outcome { + return try { + context.startActivity(intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)) + Outcome.Ok(Unit) + } catch (e: Exception) { + Outcome.Error(KmpSettingsScreenOpenerError.Unknown) + } + } +} diff --git a/src/commonMain/kotlin/com/outsidesource/oskitkmp/lib/EnumSerializer.kt b/src/commonMain/kotlin/com/outsidesource/oskitkmp/lib/EnumSerializer.kt index eafd5ea2..939987d7 100644 --- a/src/commonMain/kotlin/com/outsidesource/oskitkmp/lib/EnumSerializer.kt +++ b/src/commonMain/kotlin/com/outsidesource/oskitkmp/lib/EnumSerializer.kt @@ -20,7 +20,7 @@ import kotlin.enums.enumEntries inline fun > EnumSerializer( crossinline value: (T) -> Any, default: T? = null, -) : KSerializer = object : KSerializer { +): KSerializer = object : KSerializer { private val entries = enumEntries() private val valueMap = entries.associateBy { value(it) } @@ -72,6 +72,8 @@ inline fun > EnumSerializer( @OptIn(ExperimentalSerializationApi::class) override fun deserialize(decoder: Decoder): T { val raw = decoder.decodeAny() - return valueMap[raw] ?: default ?: throw SerializationException("Unknown enum value '$raw' for ${descriptor.serialName}") + return valueMap[raw] ?: default ?: throw SerializationException( + "Unknown enum value '$raw' for ${descriptor.serialName}", + ) } } diff --git a/src/commonMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreenOpener.kt b/src/commonMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreenOpener.kt new file mode 100644 index 00000000..28f138a1 --- /dev/null +++ b/src/commonMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreenOpener.kt @@ -0,0 +1,57 @@ +package com.outsidesource.oskitkmp.systemui + +import com.outsidesource.oskitkmp.capability.KmpCapabilityContext +import com.outsidesource.oskitkmp.outcome.Outcome + +interface IKmpSettingsScreenOpener { + suspend fun open( + context: KmpCapabilityContext, + type: SettingsScreenType = SettingsScreenType.App, + fallbackToAppSettings: Boolean = true, + ): + Outcome +} + +/** + * Represents a cross-platform settings screen opener implementation. + * + * This class allows opening specific types of settings screens + * defined by the [SettingsScreenType] enum, such as application settings, + * system settings, Bluetooth settings, or location settings. + * Depending on the platform's support and implementation, not all screen types + * may be available. + * + * On platforms that do not support this functionality, the method will return + * an error with `[KmpSettingsScreenOpenerError.UnsupportedPlatform]`. + * + * Usage: + * - Create platform-specifc instances of this class via DI or expect/actual helper and use in your common module + * + * Functions: + * - `open`: Opens the specified settings screen type. + * - Arguments: + * - `type`: The type of settings screen to open. + * - `fallbackToAppSettings`: Specifies whether to fall back to the app settings + * when the requested type is not supported. + * - Returns: + * - Outcome representing the success or failure. + */ +expect object KmpSettingsScreenOpener : IKmpSettingsScreenOpener { + override suspend fun open( + context: KmpCapabilityContext, + type: SettingsScreenType, + fallbackToAppSettings: Boolean, + ): Outcome +} + +enum class SettingsScreenType { + App, + SystemSettings, + Bluetooth, + Location, +} + +enum class KmpSettingsScreenOpenerError { + UnsupportedPlatform, + Unknown, +} diff --git a/src/iosMain/kotlin/com/outsidesource/oskitkmp/capability/KmpCapabilities.ios.kt b/src/iosMain/kotlin/com/outsidesource/oskitkmp/capability/KmpCapabilities.ios.kt index 33d9746f..af4e5e5d 100644 --- a/src/iosMain/kotlin/com/outsidesource/oskitkmp/capability/KmpCapabilities.ios.kt +++ b/src/iosMain/kotlin/com/outsidesource/oskitkmp/capability/KmpCapabilities.ios.kt @@ -1,11 +1,8 @@ package com.outsidesource.oskitkmp.capability import com.outsidesource.oskitkmp.outcome.Outcome -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import platform.Foundation.NSURL -import platform.UIKit.UIApplication -import platform.UIKit.UIApplicationOpenSettingsURLString +import com.outsidesource.oskitkmp.systemui.KmpSettingsScreenOpener +import com.outsidesource.oskitkmp.systemui.SettingsScreenType actual class KmpCapabilityContext() @@ -17,12 +14,10 @@ internal actual fun createPlatformLocationCapability(flags: Array = withContext(Dispatchers.Main) { - try { - val settingsUrl: NSURL = NSURL.URLWithString(UIApplicationOpenSettingsURLString)!! - UIApplication.sharedApplication.openURL(settingsUrl, emptyMap(), null) - Outcome.Ok(Unit) - } catch (e: Throwable) { - Outcome.Error(Unit) +): Outcome { + return if (context != null) { + KmpSettingsScreenOpener.open(context, SettingsScreenType.App) + } else { + Outcome.Error(KmpCapabilitiesError.Uninitialized) } } diff --git a/src/iosMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreen.ios.kt b/src/iosMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreen.ios.kt new file mode 100644 index 00000000..0c22b1f2 --- /dev/null +++ b/src/iosMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreen.ios.kt @@ -0,0 +1,44 @@ +package com.outsidesource.oskitkmp.systemui + +import com.outsidesource.oskitkmp.capability.KmpCapabilityContext +import com.outsidesource.oskitkmp.outcome.Outcome +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import platform.Foundation.NSURL +import platform.UIKit.UIApplication +import platform.UIKit.UIApplicationOpenSettingsURLString + +actual object KmpSettingsScreenOpener : IKmpSettingsScreenOpener { + actual override suspend fun open( + context: KmpCapabilityContext, + type: SettingsScreenType, + fallbackToAppSettings: Boolean, + ): Outcome { + val res = when (type) { + SettingsScreenType.App -> openAppSettings() + else -> Outcome.Error(KmpSettingsScreenOpenerError.UnsupportedPlatform) + } + + return if (res is Outcome.Error && type != SettingsScreenType.App && fallbackToAppSettings) { + openAppSettings() + } else { + res + } + } + + private suspend fun openAppSettings(): Outcome { + return withContext(Dispatchers.Main) { + try { + val url = NSURL(string = UIApplicationOpenSettingsURLString) + if (UIApplication.sharedApplication.canOpenURL(url)) { + UIApplication.sharedApplication.openURL(url, emptyMap(), null) + Outcome.Ok(Unit) + } else { + Outcome.Error(KmpSettingsScreenOpenerError.UnsupportedPlatform) + } + } catch (e: Throwable) { + Outcome.Error(KmpSettingsScreenOpenerError.Unknown) + } + } + } +} diff --git a/src/jvmMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreen.jvm.kt b/src/jvmMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreen.jvm.kt new file mode 100644 index 00000000..f9ba088c --- /dev/null +++ b/src/jvmMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreen.jvm.kt @@ -0,0 +1,14 @@ +package com.outsidesource.oskitkmp.systemui + +import com.outsidesource.oskitkmp.capability.KmpCapabilityContext +import com.outsidesource.oskitkmp.outcome.Outcome + +actual object KmpSettingsScreenOpener : IKmpSettingsScreenOpener { + actual override suspend fun open( + context: KmpCapabilityContext, + type: SettingsScreenType, + fallbackToAppSettings: Boolean, + ): Outcome { + return Outcome.Error(KmpSettingsScreenOpenerError.UnsupportedPlatform) + } +} diff --git a/src/wasmJsMain/kotlin/com/outsidesource/oskitkmp/capability/KmpCapabilities.wasm.kt b/src/wasmJsMain/kotlin/com/outsidesource/oskitkmp/capability/KmpCapabilities.wasm.kt index e25a75cd..5d65e0c5 100644 --- a/src/wasmJsMain/kotlin/com/outsidesource/oskitkmp/capability/KmpCapabilities.wasm.kt +++ b/src/wasmJsMain/kotlin/com/outsidesource/oskitkmp/capability/KmpCapabilities.wasm.kt @@ -1,6 +1,8 @@ package com.outsidesource.oskitkmp.capability import com.outsidesource.oskitkmp.outcome.Outcome +import com.outsidesource.oskitkmp.systemui.KmpSettingsScreenOpener +import com.outsidesource.oskitkmp.systemui.SettingsScreenType actual class KmpCapabilityContext() diff --git a/src/wasmJsMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreen.wasmJs.kt b/src/wasmJsMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreen.wasmJs.kt new file mode 100644 index 00000000..f9ba088c --- /dev/null +++ b/src/wasmJsMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreen.wasmJs.kt @@ -0,0 +1,14 @@ +package com.outsidesource.oskitkmp.systemui + +import com.outsidesource.oskitkmp.capability.KmpCapabilityContext +import com.outsidesource.oskitkmp.outcome.Outcome + +actual object KmpSettingsScreenOpener : IKmpSettingsScreenOpener { + actual override suspend fun open( + context: KmpCapabilityContext, + type: SettingsScreenType, + fallbackToAppSettings: Boolean, + ): Outcome { + return Outcome.Error(KmpSettingsScreenOpenerError.UnsupportedPlatform) + } +}