From 9fe4b45dc9479584f21d6119d07b7ee643726e37 Mon Sep 17 00:00:00 2001 From: Dr Watson <> Date: Tue, 13 May 2025 15:04:28 +0300 Subject: [PATCH 01/10] settings opener --- .../capability/KmpCapabilities.android.kt | 10 +- .../systemui/KmpSettingsScreen.android.kt | 72 +++++++++++ .../oskitkmp/systemui/KmpSettingsScreen.kt | 50 ++++++++ .../capability/KmpCapabilities.ios.kt | 14 +-- .../systemui/KmpSettingsScreen.ios.kt | 50 ++++++++ .../capability/KmpCapabilities.jvm.kt | 6 +- .../systemui/KmpSettingsScreen.jvm.kt | 119 ++++++++++++++++++ .../capability/KmpCapabilities.wasm.kt | 6 +- .../systemui/KmpSettingsScreen.wasmJs.kt | 12 ++ 9 files changed, 321 insertions(+), 18 deletions(-) create mode 100644 src/androidMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreen.android.kt create mode 100644 src/commonMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreen.kt create mode 100644 src/iosMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreen.ios.kt create mode 100644 src/jvmMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreen.jvm.kt create mode 100644 src/wasmJsMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreen.wasmJs.kt 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..c35ac685 100644 --- a/src/androidMain/kotlin/com/outsidesource/oskitkmp/capability/KmpCapabilities.android.kt +++ b/src/androidMain/kotlin/com/outsidesource/oskitkmp/capability/KmpCapabilities.android.kt @@ -5,6 +5,8 @@ import android.net.Uri import android.provider.Settings import androidx.activity.ComponentActivity import com.outsidesource.oskitkmp.outcome.Outcome +import com.outsidesource.oskitkmp.systemui.KmpSettingsScreen +import com.outsidesource.oskitkmp.systemui.SettingsScreenType actual class KmpCapabilityContext( var activity: ComponentActivity, @@ -18,12 +20,8 @@ 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) - } - activity.startActivity(intent) - return Outcome.Ok(Unit) + return KmpSettingsScreen(context?.activity ?: return Outcome.Error(KmpCapabilitiesError.Uninitialized)) + .open(SettingsScreenType.App) } 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..9e942273 --- /dev/null +++ b/src/androidMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreen.android.kt @@ -0,0 +1,72 @@ +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.outcome.Outcome + +actual class KmpSettingsScreen(private val context: Context) : IKmpSettingsScreenOpener { + actual override suspend fun open( + type: SettingsScreenType, + fallbackToAppSettings: Boolean, + ): Outcome { + val res = when (type) { + SettingsScreenType.App -> openAppSettings() + SettingsScreenType.SystemSettings -> openSystemSettings() + SettingsScreenType.Bluetooth -> openBluetoothSettings() + SettingsScreenType.Location -> openLocationSettings() + } + + return if (res is Outcome.Error && type != SettingsScreenType.App && fallbackToAppSettings) { + openAppSettings() + } else { + res + } + } + + private fun openAppSettings(): Outcome { + return try { + val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { + data = Uri.fromParts("package", context.packageName, null) + }.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + context.startActivity(intent) + Outcome.Ok(Unit) + } catch (e: Exception) { + Outcome.Error(KmpSettingsScreenError.InternalPlatformException) + } + } + + private fun openBluetoothSettings(): Outcome { + return try { + val intent = Intent(Settings.ACTION_BLUETOOTH_SETTINGS) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + context.startActivity(intent) + Outcome.Ok(Unit) + } catch (e: Exception) { + Outcome.Error(KmpSettingsScreenError.InternalPlatformException) + } + } + + private fun openLocationSettings(): Outcome { + return try { + val intent = Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + context.startActivity(intent) + Outcome.Ok(Unit) + } catch (e: Exception) { + Outcome.Error(KmpSettingsScreenError.InternalPlatformException) + } + } + + private fun openSystemSettings(): Outcome { + return try { + val intent = Intent(Settings.ACTION_SETTINGS) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + context.startActivity(intent) + Outcome.Ok(Unit) + } catch (e: Exception) { + Outcome.Error(KmpSettingsScreenError.InternalPlatformException) + } + } +} \ No newline at end of file diff --git a/src/commonMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreen.kt b/src/commonMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreen.kt new file mode 100644 index 00000000..11e7e401 --- /dev/null +++ b/src/commonMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreen.kt @@ -0,0 +1,50 @@ +package com.outsidesource.oskitkmp.systemui + +import com.outsidesource.oskitkmp.outcome.Outcome + +interface IKmpSettingsScreenOpener { + suspend fun open(type: SettingsScreenType, 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 `KmpSettingsScreenError.NotSupportedByThisPlatform`. + * + * 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 success or failure. + */ +expect class KmpSettingsScreen: IKmpSettingsScreenOpener { + override suspend fun open( + type: SettingsScreenType, + fallbackToAppSettings: Boolean, + ): Outcome +} + +enum class SettingsScreenType { + App, + SystemSettings, + Bluetooth, + Location, +} + +enum class KmpSettingsScreenError { + NotSupportedByThisPlatform, + InternalPlatformException +} \ No newline at end of file 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..29d6b62f 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,10 @@ package com.outsidesource.oskitkmp.capability import com.outsidesource.oskitkmp.outcome.Outcome +import com.outsidesource.oskitkmp.systemui.KmpSettingsScreen +import com.outsidesource.oskitkmp.systemui.SettingsScreenType import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import platform.Foundation.NSURL -import platform.UIKit.UIApplication -import platform.UIKit.UIApplicationOpenSettingsURLString actual class KmpCapabilityContext() @@ -18,11 +17,6 @@ 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) - } + KmpSettingsScreen().open(SettingsScreenType.App) } + 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..f45aa72f --- /dev/null +++ b/src/iosMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreen.ios.kt @@ -0,0 +1,50 @@ +package com.outsidesource.oskitkmp.systemui + +import com.outsidesource.oskitkmp.outcome.Outcome +import platform.Foundation.NSURL +import platform.UIKit.UIApplication +import platform.UIKit.UIApplicationOpenSettingsURLString + +actual class KmpSettingsScreen : IKmpSettingsScreenOpener { + actual override suspend fun open( + type: SettingsScreenType, + fallbackToAppSettings: Boolean, + ): Outcome { + val res = when (type) { + SettingsScreenType.App -> openAppSettings() + SettingsScreenType.SystemSettings -> launchUrl("App-prefs:") + SettingsScreenType.Bluetooth -> launchUrl("App-prefs:Bluetooth") + SettingsScreenType.Location -> launchUrl("App-prefs:Privacy&path=LOCATION") + } + + return if (res is Outcome.Error && type != SettingsScreenType.App && fallbackToAppSettings) { + openAppSettings() + } else { + res + } + } + + private fun openAppSettings(): Outcome { + return try { + val settingsUrl: NSURL = NSURL.URLWithString(UIApplicationOpenSettingsURLString)!! + UIApplication.sharedApplication.openURL(settingsUrl, emptyMap(), null) + Outcome.Ok(Unit) + } catch (e: Throwable) { + Outcome.Error(KmpSettingsScreenError.InternalPlatformException) + } + } + + private fun launchUrl(urlString: String): Outcome { + return try { + val url = NSURL(string = urlString) + if (UIApplication.sharedApplication.canOpenURL(url)) { + UIApplication.sharedApplication.openURL(url) + Outcome.Ok(Unit) + } else { + Outcome.Error(KmpSettingsScreenError.NotSupportedByThisPlatform) + } + } catch (_: Throwable) { + Outcome.Error(KmpSettingsScreenError.InternalPlatformException) + } + } +} \ No newline at end of file diff --git a/src/jvmMain/kotlin/com/outsidesource/oskitkmp/capability/KmpCapabilities.jvm.kt b/src/jvmMain/kotlin/com/outsidesource/oskitkmp/capability/KmpCapabilities.jvm.kt index e25a75cd..f11f9033 100644 --- a/src/jvmMain/kotlin/com/outsidesource/oskitkmp/capability/KmpCapabilities.jvm.kt +++ b/src/jvmMain/kotlin/com/outsidesource/oskitkmp/capability/KmpCapabilities.jvm.kt @@ -1,6 +1,8 @@ package com.outsidesource.oskitkmp.capability import com.outsidesource.oskitkmp.outcome.Outcome +import com.outsidesource.oskitkmp.systemui.KmpSettingsScreen +import com.outsidesource.oskitkmp.systemui.SettingsScreenType actual class KmpCapabilityContext() @@ -12,4 +14,6 @@ internal actual fun createPlatformLocationCapability(flags: Array = Outcome.Error(KmpCapabilitiesError.UnsupportedOperation) +): Outcome { + return KmpSettingsScreen().open(SettingsScreenType.App) +} 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..7a56e2ae --- /dev/null +++ b/src/jvmMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreen.jvm.kt @@ -0,0 +1,119 @@ +package com.outsidesource.oskitkmp.systemui + +import com.outsidesource.oskitkmp.lib.Platform +import com.outsidesource.oskitkmp.lib.current +import com.outsidesource.oskitkmp.outcome.Outcome + +actual class KmpSettingsScreen : IKmpSettingsScreenOpener { + actual override suspend fun open( + type: SettingsScreenType, + fallbackToAppSettings: Boolean, + ): Outcome { + return try { + when (Platform.current) { + Platform.Windows -> openWindowsSettings(type) + Platform.MacOS -> openMacSettings(type) + Platform.Linux -> openLinuxSettings(type) + else -> Outcome.Error(KmpSettingsScreenError.NotSupportedByThisPlatform) + } + } catch (_: Exception) { + Outcome.Error(KmpSettingsScreenError.InternalPlatformException) + } + } + + private fun openWindowsSettings(type: SettingsScreenType): Outcome { + return when (type) { + SettingsScreenType.App -> Outcome.Error(KmpSettingsScreenError.NotSupportedByThisPlatform) + SettingsScreenType.SystemSettings -> try { + Runtime.getRuntime().exec(arrayOf("cmd", "/c", "start", "ms-settings:")) + Outcome.Ok(Unit) + } catch (_: Exception) { + Outcome.Error(KmpSettingsScreenError.InternalPlatformException) + } + + SettingsScreenType.Bluetooth -> try { + Runtime.getRuntime().exec(arrayOf("cmd", "/c", "start", "ms-settings:bluetooth")) + Outcome.Ok(Unit) + } catch (_: Exception) { + Outcome.Error(KmpSettingsScreenError.InternalPlatformException) + } + + SettingsScreenType.Location -> try { + Runtime.getRuntime().exec(arrayOf("cmd", "/c", "start", "ms-settings:privacy-location")) + Outcome.Ok(Unit) + } catch (_: Exception) { + Outcome.Error(KmpSettingsScreenError.InternalPlatformException) + } + } + } + + private fun openMacSettings(type: SettingsScreenType): Outcome { + return when (type) { + SettingsScreenType.App -> Outcome.Error(KmpSettingsScreenError.NotSupportedByThisPlatform) + SettingsScreenType.SystemSettings -> try { + Runtime.getRuntime() + .exec(arrayOf("osascript", "-e", "tell application \"System Preferences\" to activate")) + Outcome.Ok(Unit) + } catch (_: Exception) { + Outcome.Error(KmpSettingsScreenError.InternalPlatformException) + } + + SettingsScreenType.Bluetooth -> try { + Runtime.getRuntime().exec( + arrayOf( + "osascript", "-e", + "tell application \"System Preferences\"\n" + + "reveal anchor \"Bluetooth\" of pane id \"com.apple.preferences.Bluetooth\"\n" + + "activate\n" + + "end tell", + ), + ) + Outcome.Ok(Unit) + } catch (_: Exception) { + Outcome.Error(KmpSettingsScreenError.InternalPlatformException) + } + + SettingsScreenType.Location -> try { + Runtime.getRuntime().exec( + arrayOf( + "osascript", "-e", + "tell application \"System Preferences\"\n" + + "reveal anchor \"Privacy_LocationServices\" of pane id \"com.apple.preference.security\"\n" + + "activate\n" + + "end tell", + ), + ) + Outcome.Ok(Unit) + } catch (_: Exception) { + Outcome.Error(KmpSettingsScreenError.InternalPlatformException) + } + } + } + + //TODO: Detect and support more desktop envs like KDE, etc.... + private fun openLinuxSettings(type: SettingsScreenType): Outcome { + return when (type) { + SettingsScreenType.App -> Outcome.Error(KmpSettingsScreenError.NotSupportedByThisPlatform) + SettingsScreenType.SystemSettings -> try { + Runtime.getRuntime().exec("gnome-control-center") + Outcome.Ok(Unit) + } catch (_: Exception) { + Outcome.Error(KmpSettingsScreenError.InternalPlatformException) + } + + SettingsScreenType.Bluetooth -> try { + Runtime.getRuntime().exec("gnome-control-center bluetooth") + Outcome.Ok(Unit) + } catch (_: Exception) { + Outcome.Error(KmpSettingsScreenError.InternalPlatformException) + } + + SettingsScreenType.Location -> try { + Runtime.getRuntime().exec("gnome-control-center privacy") + Outcome.Ok(Unit) + } catch (_: Exception) { + Outcome.Error(KmpSettingsScreenError.InternalPlatformException) + } + } + } +} \ No newline at end of file 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..f11f9033 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.KmpSettingsScreen +import com.outsidesource.oskitkmp.systemui.SettingsScreenType actual class KmpCapabilityContext() @@ -12,4 +14,6 @@ internal actual fun createPlatformLocationCapability(flags: Array = Outcome.Error(KmpCapabilitiesError.UnsupportedOperation) +): Outcome { + return KmpSettingsScreen().open(SettingsScreenType.App) +} 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..808d0154 --- /dev/null +++ b/src/wasmJsMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreen.wasmJs.kt @@ -0,0 +1,12 @@ +package com.outsidesource.oskitkmp.systemui + +import com.outsidesource.oskitkmp.outcome.Outcome + +actual class KmpSettingsScreen : IKmpSettingsScreenOpener { + actual override suspend fun open( + type: SettingsScreenType, + fallbackToAppSettings: Boolean + ): Outcome { + return Outcome.Error(KmpSettingsScreenError.NotSupportedByThisPlatform) + } +} \ No newline at end of file From 4fbc21de2c69a7fad9d6d1e7d29eb81b5c80c4dd Mon Sep 17 00:00:00 2001 From: Dr Watson <> Date: Wed, 14 May 2025 14:16:01 +0300 Subject: [PATCH 02/10] docs added --- .../com/outsidesource/oskitkmp/systemui/KmpSettingsScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commonMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreen.kt b/src/commonMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreen.kt index 11e7e401..0abc29e5 100644 --- a/src/commonMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreen.kt +++ b/src/commonMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreen.kt @@ -20,7 +20,7 @@ interface IKmpSettingsScreenOpener { * * 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: From 37955a9654a80e04f0dc14f3fc27e386a3d63205 Mon Sep 17 00:00:00 2001 From: Dr Watson <> Date: Wed, 14 May 2025 14:19:55 +0300 Subject: [PATCH 03/10] dev version up for demo project tests --- version.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.properties b/version.properties index dab1625a..e3fd4f06 100644 --- a/version.properties +++ b/version.properties @@ -1,2 +1,2 @@ #Mon Oct 11 13:40:32 EDT 2021 -version=5.0.0 +version=5.0.1 From 64cac7175ec3ec30150bae944ea1ce3d97085024 Mon Sep 17 00:00:00 2001 From: Dr Watson <> Date: Wed, 14 May 2025 14:22:46 +0300 Subject: [PATCH 04/10] lint feedback fix --- .../capability/KmpCapabilities.android.kt | 3 --- .../systemui/KmpSettingsScreen.android.kt | 2 +- .../oskitkmp/systemui/KmpSettingsScreen.kt | 9 ++++---- .../capability/KmpCapabilities.ios.kt | 1 - .../systemui/KmpSettingsScreen.ios.kt | 2 +- .../systemui/KmpSettingsScreen.jvm.kt | 23 +++++++++++-------- .../systemui/KmpSettingsScreen.wasmJs.kt | 4 ++-- 7 files changed, 22 insertions(+), 22 deletions(-) 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 c35ac685..a5d5c2f7 100644 --- a/src/androidMain/kotlin/com/outsidesource/oskitkmp/capability/KmpCapabilities.android.kt +++ b/src/androidMain/kotlin/com/outsidesource/oskitkmp/capability/KmpCapabilities.android.kt @@ -1,8 +1,5 @@ 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.KmpSettingsScreen diff --git a/src/androidMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreen.android.kt b/src/androidMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreen.android.kt index 9e942273..62d27449 100644 --- a/src/androidMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreen.android.kt +++ b/src/androidMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreen.android.kt @@ -69,4 +69,4 @@ actual class KmpSettingsScreen(private val context: Context) : IKmpSettingsScree Outcome.Error(KmpSettingsScreenError.InternalPlatformException) } } -} \ No newline at end of file +} diff --git a/src/commonMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreen.kt b/src/commonMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreen.kt index 0abc29e5..0aa887b3 100644 --- a/src/commonMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreen.kt +++ b/src/commonMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreen.kt @@ -3,7 +3,8 @@ package com.outsidesource.oskitkmp.systemui import com.outsidesource.oskitkmp.outcome.Outcome interface IKmpSettingsScreenOpener { - suspend fun open(type: SettingsScreenType, fallbackToAppSettings: Boolean = true): Outcome + suspend fun open(type: SettingsScreenType, fallbackToAppSettings: Boolean = true): + Outcome } /** @@ -30,7 +31,7 @@ interface IKmpSettingsScreenOpener { * - Returns: * - `Outcome` representing success or failure. */ -expect class KmpSettingsScreen: IKmpSettingsScreenOpener { +expect class KmpSettingsScreen : IKmpSettingsScreenOpener { override suspend fun open( type: SettingsScreenType, fallbackToAppSettings: Boolean, @@ -46,5 +47,5 @@ enum class SettingsScreenType { enum class KmpSettingsScreenError { NotSupportedByThisPlatform, - InternalPlatformException -} \ No newline at end of file + InternalPlatformException, +} 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 29d6b62f..6b6928fd 100644 --- a/src/iosMain/kotlin/com/outsidesource/oskitkmp/capability/KmpCapabilities.ios.kt +++ b/src/iosMain/kotlin/com/outsidesource/oskitkmp/capability/KmpCapabilities.ios.kt @@ -19,4 +19,3 @@ internal actual suspend fun internalOpenAppSettingsScreen( ): Outcome = withContext(Dispatchers.Main) { KmpSettingsScreen().open(SettingsScreenType.App) } - diff --git a/src/iosMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreen.ios.kt b/src/iosMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreen.ios.kt index f45aa72f..7dd78c78 100644 --- a/src/iosMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreen.ios.kt +++ b/src/iosMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreen.ios.kt @@ -47,4 +47,4 @@ actual class KmpSettingsScreen : IKmpSettingsScreenOpener { Outcome.Error(KmpSettingsScreenError.InternalPlatformException) } } -} \ No newline at end of file +} diff --git a/src/jvmMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreen.jvm.kt b/src/jvmMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreen.jvm.kt index 7a56e2ae..a85f9574 100644 --- a/src/jvmMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreen.jvm.kt +++ b/src/jvmMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreen.jvm.kt @@ -61,11 +61,12 @@ actual class KmpSettingsScreen : IKmpSettingsScreenOpener { SettingsScreenType.Bluetooth -> try { Runtime.getRuntime().exec( arrayOf( - "osascript", "-e", + "osascript", + "-e", "tell application \"System Preferences\"\n" + - "reveal anchor \"Bluetooth\" of pane id \"com.apple.preferences.Bluetooth\"\n" + - "activate\n" + - "end tell", + "reveal anchor \"Bluetooth\" of pane id \"com.apple.preferences.Bluetooth\"\n" + + "activate\n" + + "end tell", ), ) Outcome.Ok(Unit) @@ -76,11 +77,13 @@ actual class KmpSettingsScreen : IKmpSettingsScreenOpener { SettingsScreenType.Location -> try { Runtime.getRuntime().exec( arrayOf( - "osascript", "-e", + "osascript", + "-e", "tell application \"System Preferences\"\n" + - "reveal anchor \"Privacy_LocationServices\" of pane id \"com.apple.preference.security\"\n" + - "activate\n" + - "end tell", + "reveal anchor \"Privacy_LocationServices\" " + + "of pane id \"com.apple.preference.security\"\n" + + "activate\n" + + "end tell", ), ) Outcome.Ok(Unit) @@ -90,7 +93,7 @@ actual class KmpSettingsScreen : IKmpSettingsScreenOpener { } } - //TODO: Detect and support more desktop envs like KDE, etc.... + // TODO: Detect and support more desktop envs like KDE, etc.... private fun openLinuxSettings(type: SettingsScreenType): Outcome { return when (type) { SettingsScreenType.App -> Outcome.Error(KmpSettingsScreenError.NotSupportedByThisPlatform) @@ -116,4 +119,4 @@ actual class KmpSettingsScreen : IKmpSettingsScreenOpener { } } } -} \ No newline at end of file +} diff --git a/src/wasmJsMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreen.wasmJs.kt b/src/wasmJsMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreen.wasmJs.kt index 808d0154..bba5d09d 100644 --- a/src/wasmJsMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreen.wasmJs.kt +++ b/src/wasmJsMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreen.wasmJs.kt @@ -5,8 +5,8 @@ import com.outsidesource.oskitkmp.outcome.Outcome actual class KmpSettingsScreen : IKmpSettingsScreenOpener { actual override suspend fun open( type: SettingsScreenType, - fallbackToAppSettings: Boolean + fallbackToAppSettings: Boolean, ): Outcome { return Outcome.Error(KmpSettingsScreenError.NotSupportedByThisPlatform) } -} \ No newline at end of file +} From a5e771b589d30c2ebf58bb49f3b323bfc8fa6e8c Mon Sep 17 00:00:00 2001 From: Dr Watson <> Date: Tue, 20 May 2025 15:46:07 +0300 Subject: [PATCH 05/10] This is potentially a lot to support so let's mark this platform as unsupported for this feature. We'll have the code in the commit history if we ever want to come back to it. --- .../systemui/KmpSettingsScreen.jvm.kt | 116 +----------------- 1 file changed, 3 insertions(+), 113 deletions(-) diff --git a/src/jvmMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreen.jvm.kt b/src/jvmMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreen.jvm.kt index a85f9574..aca9c541 100644 --- a/src/jvmMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreen.jvm.kt +++ b/src/jvmMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreen.jvm.kt @@ -1,122 +1,12 @@ package com.outsidesource.oskitkmp.systemui -import com.outsidesource.oskitkmp.lib.Platform -import com.outsidesource.oskitkmp.lib.current import com.outsidesource.oskitkmp.outcome.Outcome -actual class KmpSettingsScreen : IKmpSettingsScreenOpener { +actual class KmpSettingsScreenOpener : IKmpSettingsScreenOpener { actual override suspend fun open( type: SettingsScreenType, fallbackToAppSettings: Boolean, - ): Outcome { - return try { - when (Platform.current) { - Platform.Windows -> openWindowsSettings(type) - Platform.MacOS -> openMacSettings(type) - Platform.Linux -> openLinuxSettings(type) - else -> Outcome.Error(KmpSettingsScreenError.NotSupportedByThisPlatform) - } - } catch (_: Exception) { - Outcome.Error(KmpSettingsScreenError.InternalPlatformException) - } - } - - private fun openWindowsSettings(type: SettingsScreenType): Outcome { - return when (type) { - SettingsScreenType.App -> Outcome.Error(KmpSettingsScreenError.NotSupportedByThisPlatform) - SettingsScreenType.SystemSettings -> try { - Runtime.getRuntime().exec(arrayOf("cmd", "/c", "start", "ms-settings:")) - Outcome.Ok(Unit) - } catch (_: Exception) { - Outcome.Error(KmpSettingsScreenError.InternalPlatformException) - } - - SettingsScreenType.Bluetooth -> try { - Runtime.getRuntime().exec(arrayOf("cmd", "/c", "start", "ms-settings:bluetooth")) - Outcome.Ok(Unit) - } catch (_: Exception) { - Outcome.Error(KmpSettingsScreenError.InternalPlatformException) - } - - SettingsScreenType.Location -> try { - Runtime.getRuntime().exec(arrayOf("cmd", "/c", "start", "ms-settings:privacy-location")) - Outcome.Ok(Unit) - } catch (_: Exception) { - Outcome.Error(KmpSettingsScreenError.InternalPlatformException) - } - } - } - - private fun openMacSettings(type: SettingsScreenType): Outcome { - return when (type) { - SettingsScreenType.App -> Outcome.Error(KmpSettingsScreenError.NotSupportedByThisPlatform) - SettingsScreenType.SystemSettings -> try { - Runtime.getRuntime() - .exec(arrayOf("osascript", "-e", "tell application \"System Preferences\" to activate")) - Outcome.Ok(Unit) - } catch (_: Exception) { - Outcome.Error(KmpSettingsScreenError.InternalPlatformException) - } - - SettingsScreenType.Bluetooth -> try { - Runtime.getRuntime().exec( - arrayOf( - "osascript", - "-e", - "tell application \"System Preferences\"\n" + - "reveal anchor \"Bluetooth\" of pane id \"com.apple.preferences.Bluetooth\"\n" + - "activate\n" + - "end tell", - ), - ) - Outcome.Ok(Unit) - } catch (_: Exception) { - Outcome.Error(KmpSettingsScreenError.InternalPlatformException) - } - - SettingsScreenType.Location -> try { - Runtime.getRuntime().exec( - arrayOf( - "osascript", - "-e", - "tell application \"System Preferences\"\n" + - "reveal anchor \"Privacy_LocationServices\" " + - "of pane id \"com.apple.preference.security\"\n" + - "activate\n" + - "end tell", - ), - ) - Outcome.Ok(Unit) - } catch (_: Exception) { - Outcome.Error(KmpSettingsScreenError.InternalPlatformException) - } - } - } - - // TODO: Detect and support more desktop envs like KDE, etc.... - private fun openLinuxSettings(type: SettingsScreenType): Outcome { - return when (type) { - SettingsScreenType.App -> Outcome.Error(KmpSettingsScreenError.NotSupportedByThisPlatform) - SettingsScreenType.SystemSettings -> try { - Runtime.getRuntime().exec("gnome-control-center") - Outcome.Ok(Unit) - } catch (_: Exception) { - Outcome.Error(KmpSettingsScreenError.InternalPlatformException) - } - - SettingsScreenType.Bluetooth -> try { - Runtime.getRuntime().exec("gnome-control-center bluetooth") - Outcome.Ok(Unit) - } catch (_: Exception) { - Outcome.Error(KmpSettingsScreenError.InternalPlatformException) - } - - SettingsScreenType.Location -> try { - Runtime.getRuntime().exec("gnome-control-center privacy") - Outcome.Ok(Unit) - } catch (_: Exception) { - Outcome.Error(KmpSettingsScreenError.InternalPlatformException) - } - } + ): Outcome { + return Outcome.Error(KmpSettingsScreenOpenerError.UnsupportedPlatform) } } From 0d17fea852475a2b6eb9f3855cd021c8fa071779 Mon Sep 17 00:00:00 2001 From: Dr Watson <> Date: Thu, 29 May 2025 13:18:07 +0300 Subject: [PATCH 06/10] PR feedback, code reading --- build.gradle.kts | 2 +- .../capability/KmpCapabilities.android.kt | 4 +- .../systemui/KmpSettingsScreen.android.kt | 64 ++++++------------- .../oskitkmp/lib/EnumSerializer.kt | 6 +- ...gsScreen.kt => KmpSettingsScreenOpener.kt} | 12 ++-- .../capability/KmpCapabilities.ios.kt | 6 +- .../systemui/KmpSettingsScreen.ios.kt | 43 ++++++------- .../capability/KmpCapabilities.jvm.kt | 6 +- .../capability/KmpCapabilities.wasm.kt | 4 +- .../systemui/KmpSettingsScreen.wasmJs.kt | 6 +- version.properties | 2 +- 11 files changed, 59 insertions(+), 96 deletions(-) rename src/commonMain/kotlin/com/outsidesource/oskitkmp/systemui/{KmpSettingsScreen.kt => KmpSettingsScreenOpener.kt} (85%) diff --git a/build.gradle.kts b/build.gradle.kts index 918abc5a..fba8fa4a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -191,7 +191,7 @@ tasks.getByName("preBuild").dependsOn("ktlintFormat") mavenPublishing { publishToMavenCentral(SonatypeHost.S01, automaticRelease = true) - signAllPublications() + //signAllPublications() configure( platform = KotlinMultiplatform( 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 a5d5c2f7..db71fc96 100644 --- a/src/androidMain/kotlin/com/outsidesource/oskitkmp/capability/KmpCapabilities.android.kt +++ b/src/androidMain/kotlin/com/outsidesource/oskitkmp/capability/KmpCapabilities.android.kt @@ -2,7 +2,7 @@ package com.outsidesource.oskitkmp.capability import androidx.activity.ComponentActivity import com.outsidesource.oskitkmp.outcome.Outcome -import com.outsidesource.oskitkmp.systemui.KmpSettingsScreen +import com.outsidesource.oskitkmp.systemui.KmpSettingsScreenOpener import com.outsidesource.oskitkmp.systemui.SettingsScreenType actual class KmpCapabilityContext( @@ -17,7 +17,7 @@ internal actual fun createPlatformLocationCapability(flags: Array { try { - return KmpSettingsScreen(context?.activity ?: return Outcome.Error(KmpCapabilitiesError.Uninitialized)) + return KmpSettingsScreenOpener(context?.activity ?: return Outcome.Error(KmpCapabilitiesError.Uninitialized)) .open(SettingsScreenType.App) } 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 index 62d27449..ef544d6e 100644 --- a/src/androidMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreen.android.kt +++ b/src/androidMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreen.android.kt @@ -6,67 +6,39 @@ import android.net.Uri import android.provider.Settings import com.outsidesource.oskitkmp.outcome.Outcome -actual class KmpSettingsScreen(private val context: Context) : IKmpSettingsScreenOpener { +actual class KmpSettingsScreenOpener(private val context: Context) : IKmpSettingsScreenOpener { actual override suspend fun open( type: SettingsScreenType, fallbackToAppSettings: Boolean, - ): Outcome { - val res = when (type) { - SettingsScreenType.App -> openAppSettings() - SettingsScreenType.SystemSettings -> openSystemSettings() - SettingsScreenType.Bluetooth -> openBluetoothSettings() - SettingsScreenType.Location -> openLocationSettings() + ): Outcome { + val appSettingsIntent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { + data = Uri.fromParts("package", context.packageName, null) } + val res = launchIntent( + context, + 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) { - openAppSettings() + launchIntent(context, appSettingsIntent) } else { res } } - private fun openAppSettings(): Outcome { - return try { - val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { - data = Uri.fromParts("package", context.packageName, null) - }.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - context.startActivity(intent) - Outcome.Ok(Unit) - } catch (e: Exception) { - Outcome.Error(KmpSettingsScreenError.InternalPlatformException) - } - } - - private fun openBluetoothSettings(): Outcome { - return try { - val intent = Intent(Settings.ACTION_BLUETOOTH_SETTINGS) - .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - context.startActivity(intent) - Outcome.Ok(Unit) - } catch (e: Exception) { - Outcome.Error(KmpSettingsScreenError.InternalPlatformException) - } - } - - private fun openLocationSettings(): Outcome { + private fun launchIntent(context: Context, intent: Intent): Outcome { return try { - val intent = Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS) - .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - context.startActivity(intent) + context.startActivity(intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)) Outcome.Ok(Unit) } catch (e: Exception) { - Outcome.Error(KmpSettingsScreenError.InternalPlatformException) + Outcome.Error(KmpSettingsScreenOpenerError.Unknown) } } - private fun openSystemSettings(): Outcome { - return try { - val intent = Intent(Settings.ACTION_SETTINGS) - .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - context.startActivity(intent) - Outcome.Ok(Unit) - } catch (e: Exception) { - Outcome.Error(KmpSettingsScreenError.InternalPlatformException) - } - } } 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/KmpSettingsScreen.kt b/src/commonMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreenOpener.kt similarity index 85% rename from src/commonMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreen.kt rename to src/commonMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreenOpener.kt index 0aa887b3..3ff3cb8f 100644 --- a/src/commonMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreen.kt +++ b/src/commonMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreenOpener.kt @@ -4,7 +4,7 @@ import com.outsidesource.oskitkmp.outcome.Outcome interface IKmpSettingsScreenOpener { suspend fun open(type: SettingsScreenType, fallbackToAppSettings: Boolean = true): - Outcome + Outcome } /** @@ -31,11 +31,11 @@ interface IKmpSettingsScreenOpener { * - Returns: * - `Outcome` representing success or failure. */ -expect class KmpSettingsScreen : IKmpSettingsScreenOpener { +expect class KmpSettingsScreenOpener : IKmpSettingsScreenOpener { override suspend fun open( type: SettingsScreenType, fallbackToAppSettings: Boolean, - ): Outcome + ): Outcome } enum class SettingsScreenType { @@ -45,7 +45,7 @@ enum class SettingsScreenType { Location, } -enum class KmpSettingsScreenError { - NotSupportedByThisPlatform, - InternalPlatformException, +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 6b6928fd..5723107c 100644 --- a/src/iosMain/kotlin/com/outsidesource/oskitkmp/capability/KmpCapabilities.ios.kt +++ b/src/iosMain/kotlin/com/outsidesource/oskitkmp/capability/KmpCapabilities.ios.kt @@ -1,7 +1,7 @@ package com.outsidesource.oskitkmp.capability import com.outsidesource.oskitkmp.outcome.Outcome -import com.outsidesource.oskitkmp.systemui.KmpSettingsScreen +import com.outsidesource.oskitkmp.systemui.KmpSettingsScreenOpener import com.outsidesource.oskitkmp.systemui.SettingsScreenType import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -16,6 +16,4 @@ internal actual fun createPlatformLocationCapability(flags: Array = withContext(Dispatchers.Main) { - KmpSettingsScreen().open(SettingsScreenType.App) -} +): Outcome = KmpSettingsScreenOpener().open(SettingsScreenType.App) diff --git a/src/iosMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreen.ios.kt b/src/iosMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreen.ios.kt index 7dd78c78..9e919d13 100644 --- a/src/iosMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreen.ios.kt +++ b/src/iosMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreen.ios.kt @@ -1,20 +1,20 @@ package com.outsidesource.oskitkmp.systemui 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 class KmpSettingsScreen : IKmpSettingsScreenOpener { +actual class KmpSettingsScreenOpener : IKmpSettingsScreenOpener { actual override suspend fun open( type: SettingsScreenType, fallbackToAppSettings: Boolean, - ): Outcome { + ): Outcome { val res = when (type) { SettingsScreenType.App -> openAppSettings() - SettingsScreenType.SystemSettings -> launchUrl("App-prefs:") - SettingsScreenType.Bluetooth -> launchUrl("App-prefs:Bluetooth") - SettingsScreenType.Location -> launchUrl("App-prefs:Privacy&path=LOCATION") + else -> Outcome.Error(KmpSettingsScreenOpenerError.UnsupportedPlatform) } return if (res is Outcome.Error && type != SettingsScreenType.App && fallbackToAppSettings) { @@ -24,27 +24,20 @@ actual class KmpSettingsScreen : IKmpSettingsScreenOpener { } } - private fun openAppSettings(): Outcome { - return try { - val settingsUrl: NSURL = NSURL.URLWithString(UIApplicationOpenSettingsURLString)!! - UIApplication.sharedApplication.openURL(settingsUrl, emptyMap(), null) - Outcome.Ok(Unit) - } catch (e: Throwable) { - Outcome.Error(KmpSettingsScreenError.InternalPlatformException) - } - } - - private fun launchUrl(urlString: String): Outcome { - return try { - val url = NSURL(string = urlString) - if (UIApplication.sharedApplication.canOpenURL(url)) { - UIApplication.sharedApplication.openURL(url) - Outcome.Ok(Unit) - } else { - Outcome.Error(KmpSettingsScreenError.NotSupportedByThisPlatform) + private suspend fun openAppSettings(): Outcome { + return withContext(Dispatchers.Main) { + try { + val url = NSURL(string = UIApplicationOpenSettingsURLString) + if (UIApplication.sharedApplication.canOpenURL(url)) { + UIApplication.sharedApplication.openURL(url) + Outcome.Ok(Unit) + } else { + Outcome.Error(KmpSettingsScreenOpenerError.UnsupportedPlatform) + } + } catch (e: Throwable) { + Outcome.Error(KmpSettingsScreenOpenerError.Unknown) } - } catch (_: Throwable) { - Outcome.Error(KmpSettingsScreenError.InternalPlatformException) } } + } diff --git a/src/jvmMain/kotlin/com/outsidesource/oskitkmp/capability/KmpCapabilities.jvm.kt b/src/jvmMain/kotlin/com/outsidesource/oskitkmp/capability/KmpCapabilities.jvm.kt index f11f9033..5d65e0c5 100644 --- a/src/jvmMain/kotlin/com/outsidesource/oskitkmp/capability/KmpCapabilities.jvm.kt +++ b/src/jvmMain/kotlin/com/outsidesource/oskitkmp/capability/KmpCapabilities.jvm.kt @@ -1,7 +1,7 @@ package com.outsidesource.oskitkmp.capability import com.outsidesource.oskitkmp.outcome.Outcome -import com.outsidesource.oskitkmp.systemui.KmpSettingsScreen +import com.outsidesource.oskitkmp.systemui.KmpSettingsScreenOpener import com.outsidesource.oskitkmp.systemui.SettingsScreenType actual class KmpCapabilityContext() @@ -14,6 +14,4 @@ internal actual fun createPlatformLocationCapability(flags: Array { - return KmpSettingsScreen().open(SettingsScreenType.App) -} +): Outcome = Outcome.Error(KmpCapabilitiesError.UnsupportedOperation) 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 f11f9033..65d81ebd 100644 --- a/src/wasmJsMain/kotlin/com/outsidesource/oskitkmp/capability/KmpCapabilities.wasm.kt +++ b/src/wasmJsMain/kotlin/com/outsidesource/oskitkmp/capability/KmpCapabilities.wasm.kt @@ -1,7 +1,7 @@ package com.outsidesource.oskitkmp.capability import com.outsidesource.oskitkmp.outcome.Outcome -import com.outsidesource.oskitkmp.systemui.KmpSettingsScreen +import com.outsidesource.oskitkmp.systemui.KmpSettingsScreenOpener import com.outsidesource.oskitkmp.systemui.SettingsScreenType actual class KmpCapabilityContext() @@ -15,5 +15,5 @@ internal actual fun createPlatformLocationCapability(flags: Array { - return KmpSettingsScreen().open(SettingsScreenType.App) + return KmpSettingsScreenOpener().open(SettingsScreenType.App) } diff --git a/src/wasmJsMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreen.wasmJs.kt b/src/wasmJsMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreen.wasmJs.kt index bba5d09d..aca9c541 100644 --- a/src/wasmJsMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreen.wasmJs.kt +++ b/src/wasmJsMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreen.wasmJs.kt @@ -2,11 +2,11 @@ package com.outsidesource.oskitkmp.systemui import com.outsidesource.oskitkmp.outcome.Outcome -actual class KmpSettingsScreen : IKmpSettingsScreenOpener { +actual class KmpSettingsScreenOpener : IKmpSettingsScreenOpener { actual override suspend fun open( type: SettingsScreenType, fallbackToAppSettings: Boolean, - ): Outcome { - return Outcome.Error(KmpSettingsScreenError.NotSupportedByThisPlatform) + ): Outcome { + return Outcome.Error(KmpSettingsScreenOpenerError.UnsupportedPlatform) } } diff --git a/version.properties b/version.properties index e3fd4f06..dab1625a 100644 --- a/version.properties +++ b/version.properties @@ -1,2 +1,2 @@ #Mon Oct 11 13:40:32 EDT 2021 -version=5.0.1 +version=5.0.0 From d2b6fbb38409056c34a8d3d21cba93f0ba465b97 Mon Sep 17 00:00:00 2001 From: Dr Watson <> Date: Thu, 29 May 2025 13:26:22 +0300 Subject: [PATCH 07/10] docs update --- .../oskitkmp/systemui/KmpSettingsScreenOpener.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/commonMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreenOpener.kt b/src/commonMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreenOpener.kt index 3ff3cb8f..964e0606 100644 --- a/src/commonMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreenOpener.kt +++ b/src/commonMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreenOpener.kt @@ -11,13 +11,13 @@ interface IKmpSettingsScreenOpener { * 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, + * 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 `KmpSettingsScreenError.NotSupportedByThisPlatform`. + * 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 @@ -29,7 +29,7 @@ interface IKmpSettingsScreenOpener { * - `fallbackToAppSettings`: Specifies whether to fall back to the app settings * when the requested type is not supported. * - Returns: - * - `Outcome` representing success or failure. + * - Outcome representing the success or failure. */ expect class KmpSettingsScreenOpener : IKmpSettingsScreenOpener { override suspend fun open( From ec9b741491411832ce500f8088d5fe5d73cdc260 Mon Sep 17 00:00:00 2001 From: Dr Watson <> Date: Thu, 29 May 2025 14:57:54 +0300 Subject: [PATCH 08/10] merge, code reading --- .../oskitkmp/systemui/KmpSettingsScreen.android.kt | 1 - .../outsidesource/oskitkmp/capability/KmpCapabilities.ios.kt | 2 -- .../outsidesource/oskitkmp/systemui/KmpSettingsScreen.ios.kt | 1 - .../outsidesource/oskitkmp/capability/KmpCapabilities.jvm.kt | 2 -- 4 files changed, 6 deletions(-) diff --git a/src/androidMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreen.android.kt b/src/androidMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreen.android.kt index ef544d6e..c2619986 100644 --- a/src/androidMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreen.android.kt +++ b/src/androidMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreen.android.kt @@ -40,5 +40,4 @@ actual class KmpSettingsScreenOpener(private val context: Context) : IKmpSetting Outcome.Error(KmpSettingsScreenOpenerError.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 5723107c..01315ca6 100644 --- a/src/iosMain/kotlin/com/outsidesource/oskitkmp/capability/KmpCapabilities.ios.kt +++ b/src/iosMain/kotlin/com/outsidesource/oskitkmp/capability/KmpCapabilities.ios.kt @@ -3,8 +3,6 @@ package com.outsidesource.oskitkmp.capability import com.outsidesource.oskitkmp.outcome.Outcome import com.outsidesource.oskitkmp.systemui.KmpSettingsScreenOpener import com.outsidesource.oskitkmp.systemui.SettingsScreenType -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext actual class KmpCapabilityContext() diff --git a/src/iosMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreen.ios.kt b/src/iosMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreen.ios.kt index 9e919d13..e9d1a303 100644 --- a/src/iosMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreen.ios.kt +++ b/src/iosMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreen.ios.kt @@ -39,5 +39,4 @@ actual class KmpSettingsScreenOpener : IKmpSettingsScreenOpener { } } } - } diff --git a/src/jvmMain/kotlin/com/outsidesource/oskitkmp/capability/KmpCapabilities.jvm.kt b/src/jvmMain/kotlin/com/outsidesource/oskitkmp/capability/KmpCapabilities.jvm.kt index 5d65e0c5..e25a75cd 100644 --- a/src/jvmMain/kotlin/com/outsidesource/oskitkmp/capability/KmpCapabilities.jvm.kt +++ b/src/jvmMain/kotlin/com/outsidesource/oskitkmp/capability/KmpCapabilities.jvm.kt @@ -1,8 +1,6 @@ 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() From 05a93396744e3e5cc7e89c9210f0784c110070cb Mon Sep 17 00:00:00 2001 From: Dr Watson <> Date: Thu, 26 Jun 2025 15:31:29 +0300 Subject: [PATCH 09/10] revert signing clause for maven publishing --- build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index fba8fa4a..918abc5a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -191,7 +191,7 @@ tasks.getByName("preBuild").dependsOn("ktlintFormat") mavenPublishing { publishToMavenCentral(SonatypeHost.S01, automaticRelease = true) - //signAllPublications() + signAllPublications() configure( platform = KotlinMultiplatform( From cb1b8e890b16b790f1305e19763fab39eca0cb3a Mon Sep 17 00:00:00 2001 From: Dr Watson <> Date: Thu, 26 Jun 2025 15:52:29 +0300 Subject: [PATCH 10/10] PR feedback --- .../oskitkmp/capability/KmpCapabilities.android.kt | 9 ++++++--- .../oskitkmp/systemui/KmpSettingsScreen.android.kt | 10 ++++++---- .../oskitkmp/systemui/KmpSettingsScreenOpener.kt | 10 ++++++++-- .../oskitkmp/capability/KmpCapabilities.ios.kt | 8 +++++++- .../oskitkmp/systemui/KmpSettingsScreen.ios.kt | 6 ++++-- .../oskitkmp/systemui/KmpSettingsScreen.jvm.kt | 4 +++- .../oskitkmp/capability/KmpCapabilities.wasm.kt | 4 +--- .../oskitkmp/systemui/KmpSettingsScreen.wasmJs.kt | 4 +++- 8 files changed, 38 insertions(+), 17 deletions(-) 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 db71fc96..aa933941 100644 --- a/src/androidMain/kotlin/com/outsidesource/oskitkmp/capability/KmpCapabilities.android.kt +++ b/src/androidMain/kotlin/com/outsidesource/oskitkmp/capability/KmpCapabilities.android.kt @@ -16,9 +16,12 @@ internal actual fun createPlatformLocationCapability(flags: Array { - try { - return KmpSettingsScreenOpener(context?.activity ?: return Outcome.Error(KmpCapabilitiesError.Uninitialized)) - .open(SettingsScreenType.App) + return try { + if (context != null) { + KmpSettingsScreenOpener.open(context, SettingsScreenType.App) + } else { + Outcome.Error(KmpCapabilitiesError.Uninitialized) + } } 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 index c2619986..7faacfb9 100644 --- a/src/androidMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreen.android.kt +++ b/src/androidMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreen.android.kt @@ -4,19 +4,21 @@ 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 class KmpSettingsScreenOpener(private val context: Context) : IKmpSettingsScreenOpener { +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.packageName, null) + data = Uri.fromParts("package", context.activity.packageName, null) } val res = launchIntent( - context, + context.activity, when (type) { SettingsScreenType.App -> appSettingsIntent SettingsScreenType.SystemSettings -> Intent(Settings.ACTION_SETTINGS) @@ -26,7 +28,7 @@ actual class KmpSettingsScreenOpener(private val context: Context) : IKmpSetting ) return if (res is Outcome.Error && type != SettingsScreenType.App && fallbackToAppSettings) { - launchIntent(context, appSettingsIntent) + launchIntent(context.activity, appSettingsIntent) } else { res } diff --git a/src/commonMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreenOpener.kt b/src/commonMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreenOpener.kt index 964e0606..28f138a1 100644 --- a/src/commonMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreenOpener.kt +++ b/src/commonMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreenOpener.kt @@ -1,9 +1,14 @@ package com.outsidesource.oskitkmp.systemui +import com.outsidesource.oskitkmp.capability.KmpCapabilityContext import com.outsidesource.oskitkmp.outcome.Outcome interface IKmpSettingsScreenOpener { - suspend fun open(type: SettingsScreenType, fallbackToAppSettings: Boolean = true): + suspend fun open( + context: KmpCapabilityContext, + type: SettingsScreenType = SettingsScreenType.App, + fallbackToAppSettings: Boolean = true, + ): Outcome } @@ -31,8 +36,9 @@ interface IKmpSettingsScreenOpener { * - Returns: * - Outcome representing the success or failure. */ -expect class KmpSettingsScreenOpener : IKmpSettingsScreenOpener { +expect object KmpSettingsScreenOpener : IKmpSettingsScreenOpener { override suspend fun open( + context: KmpCapabilityContext, type: SettingsScreenType, fallbackToAppSettings: Boolean, ): Outcome 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 01315ca6..af4e5e5d 100644 --- a/src/iosMain/kotlin/com/outsidesource/oskitkmp/capability/KmpCapabilities.ios.kt +++ b/src/iosMain/kotlin/com/outsidesource/oskitkmp/capability/KmpCapabilities.ios.kt @@ -14,4 +14,10 @@ internal actual fun createPlatformLocationCapability(flags: Array = KmpSettingsScreenOpener().open(SettingsScreenType.App) +): 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 index e9d1a303..0c22b1f2 100644 --- a/src/iosMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreen.ios.kt +++ b/src/iosMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreen.ios.kt @@ -1,5 +1,6 @@ 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 @@ -7,8 +8,9 @@ import platform.Foundation.NSURL import platform.UIKit.UIApplication import platform.UIKit.UIApplicationOpenSettingsURLString -actual class KmpSettingsScreenOpener : IKmpSettingsScreenOpener { +actual object KmpSettingsScreenOpener : IKmpSettingsScreenOpener { actual override suspend fun open( + context: KmpCapabilityContext, type: SettingsScreenType, fallbackToAppSettings: Boolean, ): Outcome { @@ -29,7 +31,7 @@ actual class KmpSettingsScreenOpener : IKmpSettingsScreenOpener { try { val url = NSURL(string = UIApplicationOpenSettingsURLString) if (UIApplication.sharedApplication.canOpenURL(url)) { - UIApplication.sharedApplication.openURL(url) + UIApplication.sharedApplication.openURL(url, emptyMap(), null) Outcome.Ok(Unit) } else { Outcome.Error(KmpSettingsScreenOpenerError.UnsupportedPlatform) diff --git a/src/jvmMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreen.jvm.kt b/src/jvmMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreen.jvm.kt index aca9c541..f9ba088c 100644 --- a/src/jvmMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreen.jvm.kt +++ b/src/jvmMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreen.jvm.kt @@ -1,9 +1,11 @@ package com.outsidesource.oskitkmp.systemui +import com.outsidesource.oskitkmp.capability.KmpCapabilityContext import com.outsidesource.oskitkmp.outcome.Outcome -actual class KmpSettingsScreenOpener : IKmpSettingsScreenOpener { +actual object KmpSettingsScreenOpener : IKmpSettingsScreenOpener { actual override suspend fun open( + context: KmpCapabilityContext, type: SettingsScreenType, fallbackToAppSettings: Boolean, ): Outcome { 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 65d81ebd..5d65e0c5 100644 --- a/src/wasmJsMain/kotlin/com/outsidesource/oskitkmp/capability/KmpCapabilities.wasm.kt +++ b/src/wasmJsMain/kotlin/com/outsidesource/oskitkmp/capability/KmpCapabilities.wasm.kt @@ -14,6 +14,4 @@ internal actual fun createPlatformLocationCapability(flags: Array { - return KmpSettingsScreenOpener().open(SettingsScreenType.App) -} +): Outcome = Outcome.Error(KmpCapabilitiesError.UnsupportedOperation) diff --git a/src/wasmJsMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreen.wasmJs.kt b/src/wasmJsMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreen.wasmJs.kt index aca9c541..f9ba088c 100644 --- a/src/wasmJsMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreen.wasmJs.kt +++ b/src/wasmJsMain/kotlin/com/outsidesource/oskitkmp/systemui/KmpSettingsScreen.wasmJs.kt @@ -1,9 +1,11 @@ package com.outsidesource.oskitkmp.systemui +import com.outsidesource.oskitkmp.capability.KmpCapabilityContext import com.outsidesource.oskitkmp.outcome.Outcome -actual class KmpSettingsScreenOpener : IKmpSettingsScreenOpener { +actual object KmpSettingsScreenOpener : IKmpSettingsScreenOpener { actual override suspend fun open( + context: KmpCapabilityContext, type: SettingsScreenType, fallbackToAppSettings: Boolean, ): Outcome {