diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..be35543 --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +MyScan \ No newline at end of file diff --git a/.idea/AndroidProjectSystem.xml b/.idea/AndroidProjectSystem.xml new file mode 100644 index 0000000..4a53bee --- /dev/null +++ b/.idea/AndroidProjectSystem.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml new file mode 100644 index 0000000..b268ef3 --- /dev/null +++ b/.idea/deploymentTargetSelector.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..639c779 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..7061a0d --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,61 @@ + + + + \ No newline at end of file diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml new file mode 100644 index 0000000..c224ad5 --- /dev/null +++ b/.idea/kotlinc.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/migrations.xml b/.idea/migrations.xml new file mode 100644 index 0000000..f8051a6 --- /dev/null +++ b/.idea/migrations.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..74dd639 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000..16660f1 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d5b52c4..e462101 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -49,6 +49,11 @@ dependencies { implementation(libs.androidx.ui.graphics) implementation(libs.androidx.ui.tooling.preview) implementation(libs.androidx.material3) + implementation(libs.androidx.camera.core) + implementation(libs.androidx.camera.camera2) + implementation(libs.androidx.camera.lifecycle) + implementation(libs.androidx.camera.view) + testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f6cab60..eae1911 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,6 +2,11 @@ + + + - Greeting( - name = "Android", - modifier = Modifier.padding(innerPadding) - ) + Scaffold(/*modifier = Modifier.fillMaxSize()*/) { innerPadding -> + Column { + Greeting(modifier = Modifier.padding(innerPadding)) + Box(/*modifier = Modifier.width(300.dp)*/) { + CameraScreen { } + } + } } } } @@ -31,9 +38,9 @@ class MainActivity : ComponentActivity() { } @Composable -fun Greeting(name: String, modifier: Modifier = Modifier) { +fun Greeting(modifier: Modifier = Modifier) { Text( - text = "Hello $name!", + text = "Scan your document", modifier = modifier ) } @@ -42,6 +49,6 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { @Composable fun GreetingPreview() { MyScanTheme { - Greeting("Android") + Greeting() } } \ No newline at end of file diff --git a/app/src/main/java/org/mydomain/myscan/view/Camera.kt b/app/src/main/java/org/mydomain/myscan/view/Camera.kt new file mode 100644 index 0000000..caf58f6 --- /dev/null +++ b/app/src/main/java/org/mydomain/myscan/view/Camera.kt @@ -0,0 +1,114 @@ +package org.mydomain.myscan.view + +import android.content.pm.PackageManager.PERMISSION_GRANTED +import android.view.ViewGroup.LayoutParams.MATCH_PARENT +import android.widget.LinearLayout +import android.widget.Toast +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.camera.core.AspectRatio.RATIO_4_3 +import androidx.camera.core.CameraSelector +import androidx.camera.core.ImageAnalysis +import androidx.camera.core.ImageProxy +import androidx.camera.core.Preview +import androidx.camera.lifecycle.ProcessCameraProvider +import androidx.camera.view.PreviewView +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +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.lifecycle.LifecycleOwner +import com.google.common.util.concurrent.ListenableFuture +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors + +@Composable +fun CameraScreen( + onImageAnalyzed: (ImageProxy) -> Unit, +) { + val context = LocalContext.current + val requestPermissionLauncher = rememberLauncherForActivityResult( + ActivityResultContracts.RequestPermission() + ) { isGranted: Boolean -> + if (!isGranted) { + Toast.makeText(context, "Camera permission was denied", Toast.LENGTH_SHORT).show() + } + } + + LaunchedEffect(Unit) { + val camera = android.Manifest.permission.CAMERA + if (ContextCompat.checkSelfPermission(context, camera) != PERMISSION_GRANTED) { + requestPermissionLauncher.launch(camera) + } + } + + CameraPreview(onImageAnalyzed = { imageProxy -> onImageAnalyzed(imageProxy) }) +} + +@Composable +fun CameraPreview( + modifier: Modifier = Modifier, + onImageAnalyzed: (ImageProxy) -> Unit, +) { + val context = LocalContext.current + val lifecycleOwner = LocalLifecycleOwner.current + val cameraProviderFuture by remember { + mutableStateOf(ProcessCameraProvider.getInstance(context)) + } + + DisposableEffect(lifecycleOwner) { + onDispose { + cameraProviderFuture.get().unbindAll() + } + } + + AndroidView(modifier = modifier, factory = { + val previewView = PreviewView(it).apply { + layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT) + scaleType = PreviewView.ScaleType.FIT_CENTER + } + + val executor = Executors.newSingleThreadExecutor() + cameraProviderFuture.addListener({ + bindCameraUseCases( + lifecycleOwner = lifecycleOwner, + cameraProviderFuture = cameraProviderFuture, + executor = executor, + previewView = previewView, + onImageAnalyzed = onImageAnalyzed + ) + }, ContextCompat.getMainExecutor(context)) + + previewView + }) +} + +fun bindCameraUseCases( + lifecycleOwner: LifecycleOwner, + cameraProviderFuture: ListenableFuture, + executor: ExecutorService, + previewView: PreviewView, + onImageAnalyzed: (ImageProxy) -> Unit, +) { + val preview: Preview = Preview.Builder().setTargetAspectRatio(RATIO_4_3).build() + + preview.surfaceProvider = previewView.surfaceProvider + + val cameraSelector: CameraSelector = + CameraSelector.Builder().requireLensFacing(CameraSelector.LENS_FACING_BACK).build() + val imageAnalysis = ImageAnalysis.Builder().setTargetAspectRatio(RATIO_4_3) + .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) + .setOutputImageFormat(ImageAnalysis.OUTPUT_IMAGE_FORMAT_RGBA_8888).build() + imageAnalysis.setAnalyzer(executor, onImageAnalyzed) + + val cameraProvider = cameraProviderFuture.get() + cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, imageAnalysis, preview) +} + diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 916e792..87e83da 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,13 +1,14 @@ [versions] -agp = "8.9.1" -kotlin = "2.0.21" +agp = "8.9.2" +kotlin = "2.1.0" coreKtx = "1.16.0" junit = "4.13.2" junitVersion = "1.2.1" espressoCore = "3.6.1" -lifecycleRuntimeKtx = "2.8.7" +lifecycleRuntimeKtx = "2.9.0" activityCompose = "1.10.1" -composeBom = "2024.09.00" +composeBom = "2025.05.00" +camerax = "1.4.2" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } @@ -25,6 +26,11 @@ androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-man androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } androidx-material3 = { group = "androidx.compose.material3", name = "material3" } +androidx-camera-core = { group = "androidx.camera", name = "camera-core", version.ref = "camerax" } +androidx-camera-camera2 = { group = "androidx.camera", name = "camera-camera2", version.ref = "camerax" } +androidx-camera-lifecycle = { group = "androidx.camera", name = "camera-lifecycle", version.ref = "camerax" } +androidx-camera-view = { group = "androidx.camera", name = "camera-view", version.ref = "camerax" } + [plugins] android-application = { id = "com.android.application", version.ref = "agp" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }