Skip to content

implement Camera permission request/query logic for Android target #7

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: release/5.1.0
Choose a base branch
from
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
@@ -0,0 +1,116 @@
package com.outsidesource.oskitkmp.capability

import android.Manifest
import android.content.pm.PackageManager
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 CameraKmpCapability : 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 = false
private var hardwareSupportsCapability = false

private val permissions = arrayOf(Manifest.permission.CAMERA)

override val hasPermissions: Boolean = true
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 {
when (it) {
Lifecycle.State.RESUMED -> send(queryStatus())
else -> {}
}
}
}

send(queryStatus())

awaitClose { }
}.distinctUntilChanged()

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

permissionResultLauncher = context.activity
.registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) {
scope.launch { permissionsResultFlow.emit(Unit) }
}
}

private fun checkHardwareSupport() {
val activity = context?.activity ?: return
hardwareSupportsCapability = activity.packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY)
}

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

if (!hardwareSupportsCapability) return CapabilityStatus.Unsupported()

val hasPermission = permissions.all {
ContextCompat.checkSelfPermission(activity, it) == PackageManager.PERMISSION_GRANTED
}

if (!hasPermission) {
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)
withContext(Dispatchers.Main) { permissionResultLauncher?.launch(permissions) }
permissionsResultFlow.firstOrNull()
hasRequestedPermissions = true
return Outcome.Ok(queryStatus())
} catch (_: 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 @@ -16,6 +16,9 @@ internal actual fun createPlatformBluetoothCapability(flags: Array<BluetoothCapa
internal actual fun createPlatformLocationCapability(flags: Array<LocationCapabilityFlags>): IKmpCapability =
LocationKmpCapability(flags)

internal actual fun createPlatformCameraCapability(): IKmpCapability =
CameraKmpCapability()

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
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 createPlatformCameraCapability(): IKmpCapability

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

val camera: IKmpCapability = createPlatformCameraCapability()

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

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 CameraKmpCapability() : 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 @@ -15,6 +15,9 @@ internal actual fun createPlatformBluetoothCapability(flags: Array<BluetoothCapa
internal actual fun createPlatformLocationCapability(flags: Array<LocationCapabilityFlags>): IKmpCapability =
LocationKmpCapability(flags)

internal actual fun createPlatformCameraCapability(): IKmpCapability =
CameraKmpCapability()

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 CameraKmpCapability() : 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 createPlatformCameraCapability(): IKmpCapability =
CameraKmpCapability()

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 CameraKmpCapability() : 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 createPlatformCameraCapability(): IKmpCapability =
CameraKmpCapability()

internal actual suspend fun internalOpenAppSettingsScreen(
context: KmpCapabilityContext?,
): Outcome<Unit, Any> = Outcome.Error(KmpCapabilitiesError.UnsupportedOperation)