diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index e99224f..3dfce0a 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -106,6 +106,7 @@ kotlin { androidMain.dependencies { implementation(compose.preview) implementation(libs.androidx.activity.compose) + implementation(libs.androidx.fragment) // to fix bug IllegalArgumentException: Can only use lower 16 bits for requestCode implementation(libs.androidx.biometric) implementation(libs.favre.bcrypt) diff --git a/composeApp/src/androidMain/kotlin/net/codinux/banking/ui/MainActivity.kt b/composeApp/src/androidMain/kotlin/net/codinux/banking/ui/MainActivity.kt index 5c04b4e..1cf314d 100644 --- a/composeApp/src/androidMain/kotlin/net/codinux/banking/ui/MainActivity.kt +++ b/composeApp/src/androidMain/kotlin/net/codinux/banking/ui/MainActivity.kt @@ -2,6 +2,7 @@ package net.codinux.banking.ui import android.os.Bundle import androidx.activity.compose.setContent +import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.runtime.Composable import androidx.compose.ui.tooling.preview.Preview import androidx.fragment.app.FragmentActivity @@ -11,6 +12,11 @@ import net.codinux.banking.ui.service.BiometricAuthenticationService import net.codinux.banking.ui.service.ImageService class MainActivity : FragmentActivity() { + + private val request = ActivityResultContracts.RequestMultiplePermissions() + + private val activityResultLauncher = registerForActivityResult(request) { } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -23,8 +29,29 @@ class MainActivity : FragmentActivity() { App() } } + + + fun requestPermissions(requiredPermissions: List): Boolean { + val requiredPermissionsArray = requiredPermissions.toTypedArray() + + activityResultLauncher.launch(requiredPermissionsArray) + + var result = request.getSynchronousResult(baseContext, requiredPermissionsArray) + while (result == null) { + result = request.getSynchronousResult(baseContext, requiredPermissionsArray) + } + + return if (result.value != null) { + val allPermissionsGranted = result.value.entries.filter { it.key in requiredPermissions }.all { it.value == true } + allPermissionsGranted + } else { + false + } + } + } + @Preview @Composable fun AppAndroidPreview() { diff --git a/composeApp/src/androidMain/kotlin/net/codinux/banking/ui/service/PermissionsService.kt b/composeApp/src/androidMain/kotlin/net/codinux/banking/ui/service/PermissionsService.kt new file mode 100644 index 0000000..b376d80 --- /dev/null +++ b/composeApp/src/androidMain/kotlin/net/codinux/banking/ui/service/PermissionsService.kt @@ -0,0 +1,13 @@ +package net.codinux.banking.ui.service + +import android.content.Context +import android.content.pm.PackageManager +import androidx.core.content.ContextCompat + +object PermissionsService { + + fun allPermissionsGranted(baseContext: Context, permissions: List) = permissions.all { + ContextCompat.checkSelfPermission(baseContext, it) == PackageManager.PERMISSION_GRANTED + } + +} \ No newline at end of file diff --git a/composeApp/src/androidMain/kotlin/net/codinux/banking/ui/service/QrCodeService.android.kt b/composeApp/src/androidMain/kotlin/net/codinux/banking/ui/service/QrCodeService.android.kt index efaff4e..aa57516 100644 --- a/composeApp/src/androidMain/kotlin/net/codinux/banking/ui/service/QrCodeService.android.kt +++ b/composeApp/src/androidMain/kotlin/net/codinux/banking/ui/service/QrCodeService.android.kt @@ -1,5 +1,6 @@ package net.codinux.banking.ui.service +import android.Manifest import androidx.camera.core.* import androidx.camera.lifecycle.ProcessCameraProvider import androidx.camera.view.PreviewView @@ -7,15 +8,14 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.viewinterop.AndroidView import androidx.core.content.ContextCompat -import androidx.fragment.app.FragmentActivity import com.google.zxing.* import com.google.zxing.common.HybridBinarizer import com.google.zxing.qrcode.QRCodeReader import net.codinux.banking.persistence.AndroidContext +import net.codinux.banking.ui.MainActivity import net.codinux.banking.ui.config.DI import net.codinux.log.logger import java.nio.ByteBuffer @@ -23,6 +23,8 @@ import java.util.concurrent.Executors actual object QrCodeService { + private val RequiredPermissions = listOf(Manifest.permission.CAMERA) + private val cameraExecutor = Executors.newCachedThreadPool() private val log by logger() @@ -32,16 +34,17 @@ actual object QrCodeService { @Composable actual fun readQrCodeFromCamera(resultCallback: (QrCodeReadResult) -> Unit) { - val context = AndroidContext.mainActivity - val cameraProviderFuture = ProcessCameraProvider.getInstance(context) + val mainActivity = LocalLifecycleOwner.current as MainActivity // we only have MainActivity, so we can be sure that LocalLifecycleOwner.current is MainActivity - val localContext = LocalContext.current - log.info { "LocalContext.current = ${localContext.javaClass} ${localContext}" } + if (PermissionsService.allPermissionsGranted(AndroidContext.applicationContext, RequiredPermissions) == false && + mainActivity.requestPermissions(RequiredPermissions) == false) { + return // we don't have the permission to start the camera + } + + val cameraProviderFuture = ProcessCameraProvider.getInstance(mainActivity) - val lifecycleOwner = LocalLifecycleOwner.current -// val context = LocalContext.current val previewView = remember { - PreviewView(context) + PreviewView(mainActivity) } cameraProviderFuture.addListener({ @@ -69,13 +72,13 @@ actual object QrCodeService { cameraProvider.unbindAll() // Bind use cases to camera - cameraProvider.bindToLifecycle(context as FragmentActivity, cameraSelector, preview, imageAnalyzer) + cameraProvider.bindToLifecycle(mainActivity, cameraSelector, preview, imageAnalyzer) } catch(e: Exception) { log.error(e) { "Use case binding failed" } } - }, ContextCompat.getMainExecutor(context)) + }, ContextCompat.getMainExecutor(mainActivity)) AndroidView(factory = { previewView }, modifier = Modifier.fillMaxSize()) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 01f1eaa..c0ba25e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -24,6 +24,7 @@ android-targetSdk = "34" androidx-activityCompose = "1.9.2" androidx-lifecycle = "2.8.2" +androidx-fragment = "1.8.3" androidx-biometric = "1.1.0" compose-plugin = "1.6.11" @@ -57,6 +58,7 @@ sqldelight-native-driver = { module = "app.cash.sqldelight:native-driver", versi androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activityCompose" } androidx-lifecycle-viewmodel = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-viewmodel", version.ref = "androidx-lifecycle" } androidx-lifecycle-runtime-compose = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "androidx-lifecycle" } +androidx-fragment = { group = "androidx.fragment", name = "fragment", version.ref = "androidx-fragment" } androidx-biometric = { group = "androidx.biometric", name = "biometric", version.ref = "androidx-biometric" } kotlinx-coroutines-swing = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" }