Skip to content
Open
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 createPlatformStorageCapability(flags: Array<StorageCapabilityFlags>): IKmpCapability =
StorageKmpCapability(flags)

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,157 @@
package com.outsidesource.oskitkmp.capability

import android.Manifest
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.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 StorageKmpCapability(
private val flags: Array<StorageCapabilityFlags>,
) : IInitializableKmpCapability, IKmpCapability {

private var context: KmpCapabilityContext? = null
private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
private var permissionResultLauncher: ActivityResultLauncher<Array<String>>? = null
private val permissionsResultFlow =
MutableSharedFlow<Unit>(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)

private var hasRequestedPermissions: Boolean = false

private val permissions = run {
val result = mutableListOf<String>()

flags.forEach { flag ->
when (flag) {
StorageCapabilityFlags.ReadExternal -> {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
result += Manifest.permission.READ_EXTERNAL_STORAGE
}
}

StorageCapabilityFlags.WriteExternal -> {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
result += Manifest.permission.WRITE_EXTERNAL_STORAGE
}
}

StorageCapabilityFlags.ReadMedia -> {
when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> {
result += Manifest.permission.READ_MEDIA_AUDIO
result += Manifest.permission.READ_MEDIA_IMAGES
result += Manifest.permission.READ_MEDIA_VIDEO
result += Manifest.permission.ACCESS_MEDIA_LOCATION
}
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> {
result += Manifest.permission.READ_EXTERNAL_STORAGE
result += Manifest.permission.ACCESS_MEDIA_LOCATION
}
else -> {
result += Manifest.permission.READ_EXTERNAL_STORAGE
}
}
}

StorageCapabilityFlags.WriteMedia -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
result += Manifest.permission.ACCESS_MEDIA_LOCATION
}
}
}
}

result.distinct().toTypedArray()
}

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.RequestMultiplePermissions()) { results ->
scope.launch {
permissionsResultFlow.emit(Unit)

if (results.all { it.value }) {
hasRequestedPermissions = false
}
}
}
}

override val hasPermissions: Boolean = permissions.isNotEmpty()
override val hasEnablableService: Boolean = false
override val supportsRequestEnable: Boolean = false
override val supportsOpenAppSettingsScreen: Boolean = true
override val supportsOpenServiceSettingsScreen: Boolean = false

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

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)
}

return CapabilityStatus.Ready
}

override suspend fun requestPermissions(): Outcome<CapabilityStatus, Any> {
try {
context?.activity ?: return Outcome.Error(KmpCapabilitiesError.Uninitialized)
hasRequestedPermissions = true
withContext(Dispatchers.Main) {
permissionResultLauncher?.launch(permissions)
}
permissionsResultFlow.firstOrNull()
return Outcome.Ok(queryStatus())
} catch (e: Exception) {
return Outcome.Error(Unit)
}
}

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> = 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 createPlatformStorageCapability(flags: Array<StorageCapabilityFlags>): IKmpCapability

/**
* [KmpCapabilities] allows querying and requesting of permissions and enablement of certain platform capabilities.
Expand All @@ -27,6 +28,7 @@ internal expect fun createPlatformLocationCapability(flags: Array<LocationCapabi
class KmpCapabilities(
bluetoothFlags: Array<BluetoothCapabilityFlags> = emptyArray(),
locationFlags: Array<LocationCapabilityFlags> = emptyArray(),
storageFlags: Array<StorageCapabilityFlags> = emptyArray(),
) {
private var context: KmpCapabilityContext? = null

Expand All @@ -46,10 +48,13 @@ class KmpCapabilities(
*/
val location: IKmpCapability = createPlatformLocationCapability(locationFlags)

val storage: IKmpCapability = createPlatformStorageCapability(storageFlags)

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

Expand All @@ -73,6 +78,13 @@ enum class LocationCapabilityFlags {
FineLocation,
}

enum class StorageCapabilityFlags {
ReadExternal,
WriteExternal,
ReadMedia,
WriteMedia,
}

interface IInitializableKmpCapability : IKmpCapability {
fun init(context: KmpCapabilityContext)
}
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 createPlatformStorageCapability(flags: Array<StorageCapabilityFlags>): IKmpCapability =
StorageKmpCapability(flags)

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,34 @@
package com.outsidesource.oskitkmp.capability

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

internal class StorageKmpCapability(
private val flags: Array<StorageCapabilityFlags>,
) : 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 createPlatformStorageCapability(flags: Array<StorageCapabilityFlags>): IKmpCapability =
StorageKmpCapability(flags)

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,34 @@
package com.outsidesource.oskitkmp.capability

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

class StorageKmpCapability(
private val flags: Array<StorageCapabilityFlags>,
) : 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 createPlatformStorageCapability(flags: Array<StorageCapabilityFlags>): IKmpCapability =
StorageKmpCapability(flags)

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,34 @@
package com.outsidesource.oskitkmp.capability

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

class StorageKmpCapability(
private val flags: Array<StorageCapabilityFlags>,
) : IInitializableKmpCapability, IKmpCapability {
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 fun init(context: KmpCapabilityContext) {}

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)
}