Skip to content
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -17,13 +16,12 @@ internal actual fun createPlatformLocationCapability(flags: Array<LocationCapabi
LocationKmpCapability(flags)

internal actual suspend fun internalOpenAppSettingsScreen(context: KmpCapabilityContext?): Outcome<Unit, Any> {
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)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Unit, KmpSettingsScreenOpenerError> {
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<Unit, KmpSettingsScreenOpenerError> {
return try {
context.startActivity(intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
Outcome.Ok(Unit)
} catch (e: Exception) {
Outcome.Error(KmpSettingsScreenOpenerError.Unknown)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import kotlin.enums.enumEntries
inline fun <reified T : Enum<T>> EnumSerializer(
crossinline value: (T) -> Any,
default: T? = null,
) : KSerializer<T> = object : KSerializer<T> {
): KSerializer<T> = object : KSerializer<T> {

private val entries = enumEntries<T>()
private val valueMap = entries.associateBy { value(it) }
Expand Down Expand Up @@ -72,6 +72,8 @@ inline fun <reified T : Enum<T>> 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}",
)
}
}
Original file line number Diff line number Diff line change
@@ -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<Unit, KmpSettingsScreenOpenerError>
}

/**
* 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<Unit, KmpSettingsScreenOpenerError>
}

enum class SettingsScreenType {
App,
SystemSettings,
Bluetooth,
Location,
}

enum class KmpSettingsScreenOpenerError {
UnsupportedPlatform,
Unknown,
}
Original file line number Diff line number Diff line change
@@ -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()

Expand All @@ -17,12 +14,10 @@ internal actual fun createPlatformLocationCapability(flags: Array<LocationCapabi

internal actual suspend fun internalOpenAppSettingsScreen(
context: KmpCapabilityContext?,
): Outcome<Unit, Any> = withContext(Dispatchers.Main) {
try {
val settingsUrl: NSURL = NSURL.URLWithString(UIApplicationOpenSettingsURLString)!!
UIApplication.sharedApplication.openURL(settingsUrl, emptyMap<Any?, Any>(), null)
Outcome.Ok(Unit)
} catch (e: Throwable) {
Outcome.Error(Unit)
): Outcome<Unit, Any> {
return if (context != null) {
KmpSettingsScreenOpener.open(context, SettingsScreenType.App)
} else {
Outcome.Error(KmpCapabilitiesError.Uninitialized)
}
}
Original file line number Diff line number Diff line change
@@ -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<Unit, KmpSettingsScreenOpenerError> {
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<Unit, KmpSettingsScreenOpenerError> {
return withContext(Dispatchers.Main) {
try {
val url = NSURL(string = UIApplicationOpenSettingsURLString)
if (UIApplication.sharedApplication.canOpenURL(url)) {
UIApplication.sharedApplication.openURL(url, emptyMap<Any?, Any>(), null)
Outcome.Ok(Unit)
} else {
Outcome.Error(KmpSettingsScreenOpenerError.UnsupportedPlatform)
}
} catch (e: Throwable) {
Outcome.Error(KmpSettingsScreenOpenerError.Unknown)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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<Unit, KmpSettingsScreenOpenerError> {
return Outcome.Error(KmpSettingsScreenOpenerError.UnsupportedPlatform)
}
}
Original file line number Diff line number Diff line change
@@ -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()

Expand Down
Original file line number Diff line number Diff line change
@@ -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<Unit, KmpSettingsScreenOpenerError> {
return Outcome.Error(KmpSettingsScreenOpenerError.UnsupportedPlatform)
}
}