Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ internal actual fun createPlatformBluetoothCapability(flags: Array<BluetoothCapa
internal actual fun createPlatformLocationCapability(flags: Array<LocationCapabilityFlags>): IKmpCapability =
LocationKmpCapability(flags)

internal actual fun createPlatformLocalNotificationsCapability(): IKmpCapability =
LocalNotificationsKmpCapability()

internal actual suspend fun internalOpenAppSettingsScreen(context: KmpCapabilityContext?): Outcome<Unit, Any> {
try {
val activity = context?.activity ?: return Outcome.Error(KmpCapabilitiesError.Uninitialized)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package com.outsidesource.oskitkmp.capability

import android.Manifest
import android.app.NotificationManager
import android.content.pm.PackageManager
import android.os.Build
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import androidx.lifecycle.Lifecycle
import com.outsidesource.oskitkmp.outcome.Outcome
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

internal class LocalNotificationsKmpCapability : IInitializableKmpCapability, IKmpCapability {

private var context: KmpCapabilityContext? = null
private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())

private var permissionResultLauncher: ActivityResultLauncher<String>? = null
private val permissionResultFlow = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
private var hasRequestedPermissions: Boolean = false

private val permission: String? = if (Build.VERSION.SDK_INT >= 33) {
Manifest.permission.POST_NOTIFICATIONS
} else {
null
}

override val hasPermissions: Boolean = permission != null
override val hasEnablableService: Boolean = false
override val supportsRequestEnable: Boolean = false
override val supportsOpenAppSettingsScreen: Boolean = true
override val supportsOpenServiceSettingsScreen: Boolean = false

override val status: Flow<CapabilityStatus> = callbackFlow {
val activity = context?.activity ?: return@callbackFlow

launch {
activity.lifecycle.currentStateFlow.collect {
if (it == Lifecycle.State.RESUMED) {
send(queryStatus())
}
}
}

send(queryStatus())

awaitClose { }
}.distinctUntilChanged()

override fun init(context: KmpCapabilityContext) {
this.context = context

permissionResultLauncher = context.activity
.registerForActivityResult(ActivityResultContracts.RequestPermission()) {
scope.launch {
permissionResultFlow.emit(Unit)
}
}
}

override suspend fun queryStatus(): CapabilityStatus {
val activity = context?.activity ?: return CapabilityStatus.Unknown

if (permission != null) {
val granted = ContextCompat.checkSelfPermission(activity, permission) ==
PackageManager.PERMISSION_GRANTED

if (!granted) {
val reason = if (hasRequestedPermissions) {
NoPermissionReason.DeniedPermanently
} else {
NoPermissionReason.NotRequested
}
return CapabilityStatus.NoPermission(reason)
}
}

val manager = ContextCompat.getSystemService(activity, NotificationManager::class.java)
if (manager?.areNotificationsEnabled() != true) {
return CapabilityStatus.NotEnabled
}

return CapabilityStatus.Ready
}

override suspend fun requestPermissions(): Outcome<CapabilityStatus, Any> {
try {
context?.activity ?: return Outcome.Error(KmpCapabilitiesError.Uninitialized)

if (permission != null) {
withContext(Dispatchers.Main) {
permissionResultLauncher?.launch(permission)
}
permissionResultFlow.firstOrNull()
hasRequestedPermissions = true
}

return Outcome.Ok(queryStatus())
} catch (e: Exception) {
return Outcome.Error(e)
}
}

override suspend fun requestEnable(): Outcome<CapabilityStatus, Any> {
return Outcome.Error(KmpCapabilitiesError.UnsupportedOperation)
}

override suspend fun openServiceSettingsScreen(): Outcome<Unit, Any> {
return Outcome.Error(KmpCapabilitiesError.UnsupportedOperation)
}

override suspend fun openAppSettingsScreen(): Outcome<Unit, Any> {
return internalOpenAppSettingsScreen(context)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ internal interface ICapabilityContextScope {
internal expect suspend fun internalOpenAppSettingsScreen(context: KmpCapabilityContext?): Outcome<Unit, Any>
internal expect fun createPlatformBluetoothCapability(flags: Array<BluetoothCapabilityFlags>): IKmpCapability
internal expect fun createPlatformLocationCapability(flags: Array<LocationCapabilityFlags>): IKmpCapability
internal expect fun createPlatformLocalNotificationsCapability(): IKmpCapability

/**
* [KmpCapabilities] allows querying and requesting of permissions and enablement of certain platform capabilities.
Expand Down Expand Up @@ -45,11 +46,13 @@ class KmpCapabilities(
* NSLocationWhenInUseUsageDescription
*/
val location: IKmpCapability = createPlatformLocationCapability(locationFlags)
val localNotifications: IKmpCapability = createPlatformLocalNotificationsCapability()

fun init(context: KmpCapabilityContext) {
this.context = context
(bluetooth as? IInitializableKmpCapability)?.init(context)
(location as? IInitializableKmpCapability)?.init(context)
(localNotifications as? IInitializableKmpCapability)?.init(context)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ internal actual fun createPlatformBluetoothCapability(flags: Array<BluetoothCapa
internal actual fun createPlatformLocationCapability(flags: Array<LocationCapabilityFlags>): IKmpCapability =
LocationKmpCapability(flags)

internal actual fun createPlatformLocalNotificationsCapability(): IKmpCapability =
LocalNotificationsKmpCapability()

internal actual suspend fun internalOpenAppSettingsScreen(
context: KmpCapabilityContext?,
): Outcome<Unit, Any> = withContext(Dispatchers.Main) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.outsidesource.oskitkmp.capability

import com.outsidesource.oskitkmp.outcome.Outcome
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow

class LocalNotificationsKmpCapability() : IInitializableKmpCapability, IKmpCapability {
override fun init(context: KmpCapabilityContext) {}

override val status: Flow<CapabilityStatus> = flow { emit(queryStatus()) }
override val hasPermissions: Boolean = false
override val hasEnablableService: Boolean = false
override val supportsRequestEnable: Boolean = false
override val supportsOpenAppSettingsScreen: Boolean = false
override val supportsOpenServiceSettingsScreen: Boolean = false

override suspend fun queryStatus(): CapabilityStatus =
CapabilityStatus.Unsupported(UnsupportedReason.NotImplemented)

override suspend fun requestPermissions(): Outcome<CapabilityStatus, Any> =
Outcome.Error(KmpCapabilitiesError.UnsupportedOperation)

override suspend fun requestEnable(): Outcome<CapabilityStatus, Any> =
Outcome.Error(KmpCapabilitiesError.UnsupportedOperation)

override suspend fun openServiceSettingsScreen(): Outcome<Unit, Any> =
Outcome.Error(KmpCapabilitiesError.UnsupportedOperation)

override suspend fun openAppSettingsScreen(): Outcome<Unit, Any> =
Outcome.Error(KmpCapabilitiesError.UnsupportedOperation)
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ internal actual fun createPlatformBluetoothCapability(flags: Array<BluetoothCapa
internal actual fun createPlatformLocationCapability(flags: Array<LocationCapabilityFlags>): IKmpCapability =
LocationKmpCapability(flags)

internal actual fun createPlatformLocalNotificationsCapability(): IKmpCapability =
LocalNotificationsKmpCapability()

internal actual suspend fun internalOpenAppSettingsScreen(
context: KmpCapabilityContext?,
): Outcome<Unit, Any> = Outcome.Error(KmpCapabilitiesError.UnsupportedOperation)
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.outsidesource.oskitkmp.capability

import com.outsidesource.oskitkmp.outcome.Outcome
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow

class LocalNotificationsKmpCapability() : IInitializableKmpCapability, IKmpCapability {
override fun init(context: KmpCapabilityContext) {}

override val status: Flow<CapabilityStatus> = flow { emit(queryStatus()) }
override val hasPermissions: Boolean = false
override val hasEnablableService: Boolean = false
override val supportsRequestEnable: Boolean = false
override val supportsOpenAppSettingsScreen: Boolean = false
override val supportsOpenServiceSettingsScreen: Boolean = false

override suspend fun queryStatus(): CapabilityStatus =
CapabilityStatus.Unsupported(UnsupportedReason.NotImplemented)

override suspend fun requestPermissions(): Outcome<CapabilityStatus, Any> =
Outcome.Error(KmpCapabilitiesError.UnsupportedOperation)

override suspend fun requestEnable(): Outcome<CapabilityStatus, Any> =
Outcome.Error(KmpCapabilitiesError.UnsupportedOperation)

override suspend fun openServiceSettingsScreen(): Outcome<Unit, Any> =
Outcome.Error(KmpCapabilitiesError.UnsupportedOperation)

override suspend fun openAppSettingsScreen(): Outcome<Unit, Any> =
Outcome.Error(KmpCapabilitiesError.UnsupportedOperation)
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ internal actual fun createPlatformBluetoothCapability(flags: Array<BluetoothCapa
internal actual fun createPlatformLocationCapability(flags: Array<LocationCapabilityFlags>): IKmpCapability =
LocationKmpCapability(flags)

internal actual fun createPlatformLocalNotificationsCapability(): IKmpCapability =
LocalNotificationsKmpCapability()

internal actual suspend fun internalOpenAppSettingsScreen(
context: KmpCapabilityContext?,
): Outcome<Unit, Any> = Outcome.Error(KmpCapabilitiesError.UnsupportedOperation)
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package com.outsidesource.oskitkmp.capability

import com.outsidesource.oskitkmp.outcome.Outcome
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import org.w3c.notifications.Notification
import org.w3c.notifications.NotificationPermission
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine

private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())

class LocalNotificationsKmpCapability : IInitializableKmpCapability, IKmpCapability {

private val localStatusFlow = MutableStateFlow<CapabilityStatus>(
if (!hardwareSupportsCapability()) {
CapabilityStatus.Unsupported()
} else {
CapabilityStatus.Unknown
},
)

override val status: Flow<CapabilityStatus> = localStatusFlow

override val hasPermissions: Boolean = true
override val hasEnablableService: Boolean = false
override val supportsRequestEnable: Boolean = false
override val supportsOpenAppSettingsScreen: Boolean = false
override val supportsOpenServiceSettingsScreen: Boolean = false

override fun init(context: KmpCapabilityContext) {
scope.launch {
if (!hardwareSupportsCapability()) return@launch

val permission = Notification.permission
localStatusFlow.value = mapPermissionToCapabilityStatus(permission)
}
}

override suspend fun queryStatus(): CapabilityStatus {
if (!hardwareSupportsCapability()) return CapabilityStatus.Unsupported()
return mapPermissionToCapabilityStatus(Notification.permission)
}

override suspend fun requestPermissions(): Outcome<CapabilityStatus, Any> {
if (!hardwareSupportsCapability()) return Outcome.Error(KmpCapabilitiesError.UnsupportedOperation)

return suspendCoroutine { continuation ->
Notification.requestPermission { permission ->
val status = mapPermissionToCapabilityStatus(permission)
localStatusFlow.value = status
continuation.resume(Outcome.Ok(status))
}
}
}

override suspend fun requestEnable(): Outcome<CapabilityStatus, Any> =
Outcome.Error(KmpCapabilitiesError.UnsupportedOperation)

override suspend fun openServiceSettingsScreen(): Outcome<Unit, Any> =
Outcome.Error(KmpCapabilitiesError.UnsupportedOperation)

override suspend fun openAppSettingsScreen(): Outcome<Unit, Any> =
Outcome.Error(KmpCapabilitiesError.UnsupportedOperation)

private fun mapPermissionToCapabilityStatus(permission: NotificationPermission): CapabilityStatus {
return when (permission.toString()) {
"granted" -> CapabilityStatus.Ready
"default" -> CapabilityStatus.NoPermission(NoPermissionReason.NotRequested)
"denied" -> CapabilityStatus.NoPermission(NoPermissionReason.DeniedPermanently)
else -> CapabilityStatus.Unknown
}
}
}

private fun hardwareSupportsCapability(): Boolean = js("""typeof Notification !== undefined""")