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..93c88a32 100644 --- a/src/androidMain/kotlin/com/outsidesource/oskitkmp/capability/KmpCapabilities.android.kt +++ b/src/androidMain/kotlin/com/outsidesource/oskitkmp/capability/KmpCapabilities.android.kt @@ -16,6 +16,9 @@ internal actual fun createPlatformBluetoothCapability(flags: Array): IKmpCapability = LocationKmpCapability(flags) +internal actual fun createPlatformNfcCapability(): IKmpCapability = + NfcKmpCapability() + internal actual suspend fun internalOpenAppSettingsScreen(context: KmpCapabilityContext?): Outcome { try { val activity = context?.activity ?: return Outcome.Error(KmpCapabilitiesError.Uninitialized) diff --git a/src/androidMain/kotlin/com/outsidesource/oskitkmp/capability/NfcCapability.android.kt b/src/androidMain/kotlin/com/outsidesource/oskitkmp/capability/NfcCapability.android.kt new file mode 100644 index 00000000..11b64f58 --- /dev/null +++ b/src/androidMain/kotlin/com/outsidesource/oskitkmp/capability/NfcCapability.android.kt @@ -0,0 +1,150 @@ +package com.outsidesource.oskitkmp.capability + +import android.Manifest +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.content.pm.PackageManager +import android.nfc.NfcAdapter +import android.os.Build +import android.provider.Settings +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.BufferOverflow +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 NfcKmpCapability : IInitializableKmpCapability, IKmpCapability { + + private var context: KmpCapabilityContext? = null + private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob()) + private var permissionResultLauncher: ActivityResultLauncher>? = null + private val permissionsResultFlow = + MutableSharedFlow(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) + + private val permissions = if (Build.VERSION.SDK_INT < 33) { + emptyArray() + } else { + arrayOf(Manifest.permission.NFC) + } + + private var nfcAdapter: NfcAdapter? = null + private var hardwareSupportsCapability = false + private var hasRequestedPermissions = false + + override val hasPermissions: Boolean = permissions.isNotEmpty() + override val hasEnablableService: Boolean = true + override val supportsRequestEnable: Boolean = false + override val supportsOpenAppSettingsScreen: Boolean = true + override val supportsOpenServiceSettingsScreen: Boolean = true + + override val status: Flow = callbackFlow { + val activity = context?.activity ?: return@callbackFlow + + val filter = IntentFilter(NfcAdapter.ACTION_ADAPTER_STATE_CHANGED) + val receiver = object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + launch { send(queryStatus()) } + } + } + ContextCompat.registerReceiver(activity, receiver, filter, ContextCompat.RECEIVER_NOT_EXPORTED) + + launch { + activity.lifecycle.currentStateFlow.collect { + when (it) { + Lifecycle.State.RESUMED -> send(queryStatus()) + else -> {} + } + } + } + + send(queryStatus()) + + awaitClose { + activity.unregisterReceiver(receiver) + } + }.distinctUntilChanged() + + override fun init(context: KmpCapabilityContext) { + this.context = context + + val activity = context.activity + hardwareSupportsCapability = activity.packageManager.hasSystemFeature(PackageManager.FEATURE_NFC) + nfcAdapter = NfcAdapter.getDefaultAdapter(activity) + + if (permissions.isNotEmpty()) { + permissionResultLauncher = activity.registerForActivityResult( + ActivityResultContracts.RequestMultiplePermissions(), + ) { + scope.launch { permissionsResultFlow.emit(Unit) } + } + } + } + + override suspend fun queryStatus(): CapabilityStatus { + val activity = context?.activity ?: return CapabilityStatus.Unknown + if (!hardwareSupportsCapability) return CapabilityStatus.Unsupported() + + val hasAuthorization = permissions.all { + ContextCompat.checkSelfPermission(activity, it) == PackageManager.PERMISSION_GRANTED + } + + if (!hasAuthorization) { + val reason = if (hasRequestedPermissions) { + NoPermissionReason.DeniedPermanently + } else { + NoPermissionReason.NotRequested + } + return CapabilityStatus.NoPermission(reason) + } + + if (nfcAdapter?.isEnabled == false) return CapabilityStatus.NotEnabled + + return CapabilityStatus.Ready + } + + override suspend fun requestPermissions(): Outcome { + try { + context?.activity ?: return Outcome.Error(KmpCapabilitiesError.Uninitialized) + withContext(Dispatchers.Main) { + permissionResultLauncher?.launch(permissions) + } + permissionsResultFlow.firstOrNull() + hasRequestedPermissions = true + return Outcome.Ok(queryStatus()) + } catch (e: Exception) { + return Outcome.Error(Unit) + } + } + + override suspend fun requestEnable(): Outcome { + return Outcome.Error(Unit) + } + + override suspend fun openServiceSettingsScreen(): Outcome { + try { + val activity = context?.activity ?: return Outcome.Error(KmpCapabilitiesError.Uninitialized) + val intent = Intent(Settings.ACTION_NFC_SETTINGS) + activity.startActivity(intent) + return Outcome.Ok(Unit) + } catch (e: Exception) { + return Outcome.Error(e) + } + } + + override suspend fun openAppSettingsScreen(): Outcome = + internalOpenAppSettingsScreen(context) +} diff --git a/src/commonMain/kotlin/com/outsidesource/oskitkmp/capability/KmpCapabilities.kt b/src/commonMain/kotlin/com/outsidesource/oskitkmp/capability/KmpCapabilities.kt index 0f913e94..ca1a3165 100644 --- a/src/commonMain/kotlin/com/outsidesource/oskitkmp/capability/KmpCapabilities.kt +++ b/src/commonMain/kotlin/com/outsidesource/oskitkmp/capability/KmpCapabilities.kt @@ -12,6 +12,7 @@ internal interface ICapabilityContextScope { internal expect suspend fun internalOpenAppSettingsScreen(context: KmpCapabilityContext?): Outcome internal expect fun createPlatformBluetoothCapability(flags: Array): IKmpCapability internal expect fun createPlatformLocationCapability(flags: Array): IKmpCapability +internal expect fun createPlatformNfcCapability(): IKmpCapability /** * [KmpCapabilities] allows querying and requesting of permissions and enablement of certain platform capabilities. @@ -46,10 +47,13 @@ class KmpCapabilities( */ val location: IKmpCapability = createPlatformLocationCapability(locationFlags) + val nfc: IKmpCapability = createPlatformNfcCapability() + fun init(context: KmpCapabilityContext) { this.context = context (bluetooth as? IInitializableKmpCapability)?.init(context) (location as? IInitializableKmpCapability)?.init(context) + (nfc as? IInitializableKmpCapability)?.init(context) } } 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..80e97ca5 100644 --- a/src/iosMain/kotlin/com/outsidesource/oskitkmp/capability/KmpCapabilities.ios.kt +++ b/src/iosMain/kotlin/com/outsidesource/oskitkmp/capability/KmpCapabilities.ios.kt @@ -15,6 +15,9 @@ internal actual fun createPlatformBluetoothCapability(flags: Array): IKmpCapability = LocationKmpCapability(flags) +internal actual fun createPlatformNfcCapability(): IKmpCapability = + NfcKmpCapability() + internal actual suspend fun internalOpenAppSettingsScreen( context: KmpCapabilityContext?, ): Outcome = withContext(Dispatchers.Main) { diff --git a/src/iosMain/kotlin/com/outsidesource/oskitkmp/capability/NfcCapability.ios.kt b/src/iosMain/kotlin/com/outsidesource/oskitkmp/capability/NfcCapability.ios.kt new file mode 100644 index 00000000..a66ca745 --- /dev/null +++ b/src/iosMain/kotlin/com/outsidesource/oskitkmp/capability/NfcCapability.ios.kt @@ -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 NfcKmpCapability : IInitializableKmpCapability, IKmpCapability { + override val status: Flow = 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 fun init(context: KmpCapabilityContext) {} + + override suspend fun queryStatus(): CapabilityStatus = CapabilityStatus.Unsupported() + + override suspend fun requestPermissions(): Outcome = + Outcome.Error(KmpCapabilitiesError.UnsupportedOperation) + + override suspend fun requestEnable(): Outcome = + Outcome.Error(KmpCapabilitiesError.UnsupportedOperation) + + override suspend fun openServiceSettingsScreen(): Outcome = + Outcome.Error(KmpCapabilitiesError.UnsupportedOperation) + + override suspend fun openAppSettingsScreen(): Outcome = + Outcome.Error(KmpCapabilitiesError.UnsupportedOperation) +} 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..738761e4 100644 --- a/src/jvmMain/kotlin/com/outsidesource/oskitkmp/capability/KmpCapabilities.jvm.kt +++ b/src/jvmMain/kotlin/com/outsidesource/oskitkmp/capability/KmpCapabilities.jvm.kt @@ -10,6 +10,9 @@ internal actual fun createPlatformBluetoothCapability(flags: Array): IKmpCapability = LocationKmpCapability(flags) +internal actual fun createPlatformNfcCapability(): IKmpCapability = + NfcKmpCapability() + internal actual suspend fun internalOpenAppSettingsScreen( context: KmpCapabilityContext?, ): Outcome = Outcome.Error(KmpCapabilitiesError.UnsupportedOperation) diff --git a/src/jvmMain/kotlin/com/outsidesource/oskitkmp/capability/NfcCapabilities.jvm.kt.kt b/src/jvmMain/kotlin/com/outsidesource/oskitkmp/capability/NfcCapabilities.jvm.kt.kt new file mode 100644 index 00000000..895a17b7 --- /dev/null +++ b/src/jvmMain/kotlin/com/outsidesource/oskitkmp/capability/NfcCapabilities.jvm.kt.kt @@ -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 NfcKmpCapability : IInitializableKmpCapability, IKmpCapability { + override fun init(context: KmpCapabilityContext) {} + + override val status: Flow = 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 = + Outcome.Error(KmpCapabilitiesError.UnsupportedOperation) + + override suspend fun requestEnable(): Outcome = + Outcome.Error(KmpCapabilitiesError.UnsupportedOperation) + + override suspend fun openServiceSettingsScreen(): Outcome = + Outcome.Error(KmpCapabilitiesError.UnsupportedOperation) + + override suspend fun openAppSettingsScreen(): 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 e25a75cd..738761e4 100644 --- a/src/wasmJsMain/kotlin/com/outsidesource/oskitkmp/capability/KmpCapabilities.wasm.kt +++ b/src/wasmJsMain/kotlin/com/outsidesource/oskitkmp/capability/KmpCapabilities.wasm.kt @@ -10,6 +10,9 @@ internal actual fun createPlatformBluetoothCapability(flags: Array): IKmpCapability = LocationKmpCapability(flags) +internal actual fun createPlatformNfcCapability(): IKmpCapability = + NfcKmpCapability() + internal actual suspend fun internalOpenAppSettingsScreen( context: KmpCapabilityContext?, ): Outcome = Outcome.Error(KmpCapabilitiesError.UnsupportedOperation) diff --git a/src/wasmJsMain/kotlin/com/outsidesource/oskitkmp/capability/NfcCapability.wasm.kt b/src/wasmJsMain/kotlin/com/outsidesource/oskitkmp/capability/NfcCapability.wasm.kt new file mode 100644 index 00000000..a66ca745 --- /dev/null +++ b/src/wasmJsMain/kotlin/com/outsidesource/oskitkmp/capability/NfcCapability.wasm.kt @@ -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 NfcKmpCapability : IInitializableKmpCapability, IKmpCapability { + override val status: Flow = 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 fun init(context: KmpCapabilityContext) {} + + override suspend fun queryStatus(): CapabilityStatus = CapabilityStatus.Unsupported() + + override suspend fun requestPermissions(): Outcome = + Outcome.Error(KmpCapabilitiesError.UnsupportedOperation) + + override suspend fun requestEnable(): Outcome = + Outcome.Error(KmpCapabilitiesError.UnsupportedOperation) + + override suspend fun openServiceSettingsScreen(): Outcome = + Outcome.Error(KmpCapabilitiesError.UnsupportedOperation) + + override suspend fun openAppSettingsScreen(): Outcome = + Outcome.Error(KmpCapabilitiesError.UnsupportedOperation) +}