From 7ad5920eeb30fe67690ebd27be727f41d47e2252 Mon Sep 17 00:00:00 2001 From: retanar Date: Wed, 20 Nov 2024 09:25:53 +0200 Subject: [PATCH 01/10] Setup empty screens and navigation --- .../featuremodule/homeImpl/HomeGraphEntry.kt | 24 ++++++++++++++++- .../homeImpl/camera/ImageUploadScreen.kt | 7 +++++ .../homeImpl/camera/TakePhotoScreen.kt | 7 +++++ .../featuremodule/homeImpl/ui/HomeContract.kt | 1 + .../featuremodule/homeImpl/ui/HomeScreen.kt | 27 +++++++++---------- .../com/featuremodule/homeImpl/ui/HomeVM.kt | 6 +++++ 6 files changed, 57 insertions(+), 15 deletions(-) create mode 100644 feature/homeImpl/src/main/java/com/featuremodule/homeImpl/camera/ImageUploadScreen.kt create mode 100644 feature/homeImpl/src/main/java/com/featuremodule/homeImpl/camera/TakePhotoScreen.kt diff --git a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/HomeGraphEntry.kt b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/HomeGraphEntry.kt index a966086..d25c32b 100644 --- a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/HomeGraphEntry.kt +++ b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/HomeGraphEntry.kt @@ -4,6 +4,8 @@ import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable import com.featuremodule.core.navigation.HIDE_NAV_BAR import com.featuremodule.homeApi.HomeDestination +import com.featuremodule.homeImpl.camera.ImageUploadScreen +import com.featuremodule.homeImpl.camera.TakePhotoScreen import com.featuremodule.homeImpl.exoplayer.ExoplayerScreen import com.featuremodule.homeImpl.ui.HomeScreen @@ -15,12 +17,32 @@ fun NavGraphBuilder.registerHome() { composable(InternalRoutes.ExoplayerDestination.ROUTE) { ExoplayerScreen() } + + composable(InternalRoutes.ImageUploadDestination.ROUTE) { + ImageUploadScreen() + } + + composable(InternalRoutes.TakePhotoDestination.ROUTE) { + TakePhotoScreen() + } } -internal sealed class InternalRoutes { +internal class InternalRoutes { object ExoplayerDestination { const val ROUTE = HIDE_NAV_BAR + "exoplayer" fun constructRoute() = ROUTE } + + object ImageUploadDestination { + const val ROUTE = "image_upload" + + fun constructRoute() = ROUTE + } + + object TakePhotoDestination { + const val ROUTE = "take_photo" + + fun constructRoute() = ROUTE + } } diff --git a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/camera/ImageUploadScreen.kt b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/camera/ImageUploadScreen.kt new file mode 100644 index 0000000..0201ad2 --- /dev/null +++ b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/camera/ImageUploadScreen.kt @@ -0,0 +1,7 @@ +package com.featuremodule.homeImpl.camera + +import androidx.compose.runtime.Composable + +@Composable +internal fun ImageUploadScreen() { +} diff --git a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/camera/TakePhotoScreen.kt b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/camera/TakePhotoScreen.kt new file mode 100644 index 0000000..e38707b --- /dev/null +++ b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/camera/TakePhotoScreen.kt @@ -0,0 +1,7 @@ +package com.featuremodule.homeImpl.camera + +import androidx.compose.runtime.Composable + +@Composable +internal fun TakePhotoScreen() { +} diff --git a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/ui/HomeContract.kt b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/ui/HomeContract.kt index e5bc0c6..3892fac 100644 --- a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/ui/HomeContract.kt +++ b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/ui/HomeContract.kt @@ -8,4 +8,5 @@ internal data object State : UiState internal sealed interface Event : UiEvent { data object NavigateToFeatureA : Event data object NavigateToExoplayer : Event + data object NavigateToCamera : Event } diff --git a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/ui/HomeScreen.kt b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/ui/HomeScreen.kt index 2b7a3e5..ec64729 100644 --- a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/ui/HomeScreen.kt +++ b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/ui/HomeScreen.kt @@ -1,11 +1,10 @@ package com.featuremodule.homeImpl.ui +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.IntrinsicSize -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.width import androidx.compose.material3.Button import androidx.compose.material3.Text @@ -25,21 +24,21 @@ internal fun HomeScreen(route: String?, viewModel: HomeVM = hiltViewModel()) { Column( modifier = Modifier.width(IntrinsicSize.Max), + verticalArrangement = Arrangement.spacedBy(24.dp), ) { - Button( - modifier = Modifier.fillMaxWidth(), - onClick = { viewModel.postEvent(Event.NavigateToFeatureA) }, - ) { - Text(text = "Pass number") + @Composable + fun GenericButton(text: String, onClick: () -> Unit) { + Button( + modifier = Modifier.fillMaxWidth(), + onClick = onClick, + ) { + Text(text = text) + } } - Spacer(modifier = Modifier.height(24.dp)) - Button( - modifier = Modifier.fillMaxWidth(), - onClick = { viewModel.postEvent(Event.NavigateToExoplayer) }, - ) { - Text(text = "Exoplayer") - } + GenericButton(text = "Pass number") { viewModel.postEvent(Event.NavigateToFeatureA) } + GenericButton(text = "Exoplayer") { viewModel.postEvent(Event.NavigateToExoplayer) } + GenericButton(text = "Camera") { viewModel.postEvent(Event.NavigateToCamera) } } } } diff --git a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/ui/HomeVM.kt b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/ui/HomeVM.kt index e274dd8..afe3d54 100644 --- a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/ui/HomeVM.kt +++ b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/ui/HomeVM.kt @@ -33,6 +33,12 @@ internal class HomeVM @Inject constructor( NavCommand.Forward(InternalRoutes.ExoplayerDestination.constructRoute()), ) } + + Event.NavigateToCamera -> launch { + navManager.navigate( + NavCommand.Forward(InternalRoutes.ImageUploadDestination.constructRoute()), + ) + } } } } From 5d72aaf9d6ae8d803039f6245982b1d1232b42a5 Mon Sep 17 00:00:00 2001 From: retanar Date: Wed, 20 Nov 2024 13:13:14 +0200 Subject: [PATCH 02/10] Added camera and media picker intents --- feature/homeImpl/build.gradle.kts | 2 + .../homeImpl/camera/ImageUploadContract.kt | 15 +++++ .../homeImpl/camera/ImageUploadScreen.kt | 60 ++++++++++++++++++- .../homeImpl/camera/ImageUploadVM.kt | 19 ++++++ 4 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 feature/homeImpl/src/main/java/com/featuremodule/homeImpl/camera/ImageUploadContract.kt create mode 100644 feature/homeImpl/src/main/java/com/featuremodule/homeImpl/camera/ImageUploadVM.kt diff --git a/feature/homeImpl/build.gradle.kts b/feature/homeImpl/build.gradle.kts index 37148a3..91ab193 100644 --- a/feature/homeImpl/build.gradle.kts +++ b/feature/homeImpl/build.gradle.kts @@ -11,4 +11,6 @@ dependencies { implementation(projects.feature.featureAApi) implementation(libs.bundles.exoplayer) + + implementation(libs.glide.compose) } diff --git a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/camera/ImageUploadContract.kt b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/camera/ImageUploadContract.kt new file mode 100644 index 0000000..a9c6ef0 --- /dev/null +++ b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/camera/ImageUploadContract.kt @@ -0,0 +1,15 @@ +package com.featuremodule.homeImpl.camera + +import android.graphics.Bitmap +import android.net.Uri +import com.featuremodule.core.ui.UiEvent +import com.featuremodule.core.ui.UiState + +internal data class State( + val image: Any? = null, +) : UiState + +internal sealed interface Event : UiEvent { + data class PhotoTaken(val bitmap: Bitmap) : Event + data class ImagePicked(val uri: Uri) : Event +} diff --git a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/camera/ImageUploadScreen.kt b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/camera/ImageUploadScreen.kt index 0201ad2..5f1e4df 100644 --- a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/camera/ImageUploadScreen.kt +++ b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/camera/ImageUploadScreen.kt @@ -1,7 +1,65 @@ package com.featuremodule.homeImpl.camera +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.PickVisualMediaRequest +import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia +import androidx.activity.result.contract.ActivityResultContracts.TakePicturePreview +import androidx.activity.result.launch +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Button +import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi +import com.bumptech.glide.integration.compose.GlideImage +@OptIn(ExperimentalGlideComposeApi::class) @Composable -internal fun ImageUploadScreen() { +internal fun ImageUploadScreen(viewModel: ImageUploadVM = hiltViewModel()) { + val context = LocalContext.current + val state by viewModel.state.collectAsStateWithLifecycle() + + val launchPhoto = rememberLauncherForActivityResult(TakePicturePreview()) { bitmap -> + bitmap?.let { viewModel.postEvent(Event.PhotoTaken(it)) } + } + val launchImagePicker = rememberLauncherForActivityResult(PickVisualMedia()) { uri -> + uri?.let { viewModel.postEvent(Event.ImagePicked(it)) } + } + + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + Column( + modifier = Modifier.width(200.dp), + ) { + GlideImage( + model = state.image, + contentDescription = null, + modifier = Modifier.size(200.dp), + ) + + Button( + onClick = { launchPhoto.launch() }, + modifier = Modifier.fillMaxWidth(), + ) { + Text(text = "Open camera app") + } + + Button( + onClick = { launchImagePicker.launch(PickVisualMediaRequest(PickVisualMedia.ImageOnly)) }, + modifier = Modifier.fillMaxWidth(), + ) { + Text(text = "Open image picker") + } + } + } } diff --git a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/camera/ImageUploadVM.kt b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/camera/ImageUploadVM.kt new file mode 100644 index 0000000..056aeec --- /dev/null +++ b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/camera/ImageUploadVM.kt @@ -0,0 +1,19 @@ +package com.featuremodule.homeImpl.camera + +import com.featuremodule.core.ui.BaseVM +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject + +@HiltViewModel +internal class ImageUploadVM @Inject constructor( + +) : BaseVM() { + override fun initialState() = State() + + override fun handleEvent(event: Event) { + when (event) { + is Event.PhotoTaken -> setState { copy(image = event.bitmap) } + is Event.ImagePicked -> setState { copy(image = event.uri) } + } + } +} From 32f8cd7d766f309c387ddef484fa1fef6d6f3bd4 Mon Sep 17 00:00:00 2001 From: retanar Date: Mon, 25 Nov 2024 13:12:47 +0200 Subject: [PATCH 03/10] Added requesting camera permission and navigation to camera screen --- feature/homeImpl/src/main/AndroidManifest.xml | 7 ++ .../featuremodule/homeImpl/HomeGraphEntry.kt | 2 +- .../homeImpl/camera/ImageUploadContract.kt | 1 + .../homeImpl/camera/ImageUploadScreen.kt | 69 ++++++++++++++----- .../homeImpl/camera/ImageUploadVM.kt | 10 ++- 5 files changed, 71 insertions(+), 18 deletions(-) create mode 100644 feature/homeImpl/src/main/AndroidManifest.xml diff --git a/feature/homeImpl/src/main/AndroidManifest.xml b/feature/homeImpl/src/main/AndroidManifest.xml new file mode 100644 index 0000000..f9618a5 --- /dev/null +++ b/feature/homeImpl/src/main/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/HomeGraphEntry.kt b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/HomeGraphEntry.kt index d25c32b..7a88b96 100644 --- a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/HomeGraphEntry.kt +++ b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/HomeGraphEntry.kt @@ -41,7 +41,7 @@ internal class InternalRoutes { } object TakePhotoDestination { - const val ROUTE = "take_photo" + const val ROUTE = HIDE_NAV_BAR + "take_photo" fun constructRoute() = ROUTE } diff --git a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/camera/ImageUploadContract.kt b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/camera/ImageUploadContract.kt index a9c6ef0..0f97918 100644 --- a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/camera/ImageUploadContract.kt +++ b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/camera/ImageUploadContract.kt @@ -12,4 +12,5 @@ internal data class State( internal sealed interface Event : UiEvent { data class PhotoTaken(val bitmap: Bitmap) : Event data class ImagePicked(val uri: Uri) : Event + data object OpenInAppCamera : Event } diff --git a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/camera/ImageUploadScreen.kt b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/camera/ImageUploadScreen.kt index 5f1e4df..5753fe6 100644 --- a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/camera/ImageUploadScreen.kt +++ b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/camera/ImageUploadScreen.kt @@ -1,8 +1,11 @@ package com.featuremodule.homeImpl.camera +import android.Manifest +import android.content.pm.PackageManager import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.PickVisualMediaRequest import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia +import androidx.activity.result.contract.ActivityResultContracts.RequestPermission import androidx.activity.result.contract.ActivityResultContracts.TakePicturePreview import androidx.activity.result.launch import androidx.compose.foundation.layout.Box @@ -19,47 +22,81 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp +import androidx.core.content.ContextCompat import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi import com.bumptech.glide.integration.compose.GlideImage -@OptIn(ExperimentalGlideComposeApi::class) @Composable internal fun ImageUploadScreen(viewModel: ImageUploadVM = hiltViewModel()) { val context = LocalContext.current val state by viewModel.state.collectAsStateWithLifecycle() - val launchPhoto = rememberLauncherForActivityResult(TakePicturePreview()) { bitmap -> + val launchSystemPhotoTaker = rememberLauncherForActivityResult(TakePicturePreview()) { bitmap -> bitmap?.let { viewModel.postEvent(Event.PhotoTaken(it)) } } val launchImagePicker = rememberLauncherForActivityResult(PickVisualMedia()) { uri -> uri?.let { viewModel.postEvent(Event.ImagePicked(it)) } } + val launchCameraPermissionRequest = + rememberLauncherForActivityResult(RequestPermission()) { isGranted -> + if (isGranted) { + viewModel.postEvent(Event.OpenInAppCamera) + } else { + // TODO("Show dialog for refused request") + } + } + + ImageUploadScreen( + state = state, + launchPhotoTaker = { launchSystemPhotoTaker.launch() }, + launchImagePicker = { + launchImagePicker.launch(PickVisualMediaRequest(PickVisualMedia.ImageOnly)) + }, + launchCamera = { + if (ContextCompat.checkSelfPermission( + context, + Manifest.permission.CAMERA, + ) == PackageManager.PERMISSION_DENIED + ) { + launchCameraPermissionRequest.launch(Manifest.permission.CAMERA) + } else { + viewModel.postEvent(Event.OpenInAppCamera) + } + }, + ) +} +@OptIn(ExperimentalGlideComposeApi::class) +@Composable +private fun ImageUploadScreen( + state: State, + launchPhotoTaker: () -> Unit, + launchImagePicker: () -> Unit, + launchCamera: () -> Unit, +) { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - Column( - modifier = Modifier.width(200.dp), - ) { + Column(modifier = Modifier.width(200.dp)) { GlideImage( model = state.image, contentDescription = null, modifier = Modifier.size(200.dp), ) - Button( - onClick = { launchPhoto.launch() }, - modifier = Modifier.fillMaxWidth(), - ) { - Text(text = "Open camera app") + @Composable + fun GenericButton(text: String, onClick: () -> Unit) { + Button( + modifier = Modifier.fillMaxWidth(), + onClick = onClick, + ) { + Text(text = text) + } } - Button( - onClick = { launchImagePicker.launch(PickVisualMediaRequest(PickVisualMedia.ImageOnly)) }, - modifier = Modifier.fillMaxWidth(), - ) { - Text(text = "Open image picker") - } + GenericButton(text = "Open camera app") { launchPhotoTaker() } + GenericButton(text = "Open image picker") { launchImagePicker() } + GenericButton(text = "Open custom camera") { launchCamera() } } } } diff --git a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/camera/ImageUploadVM.kt b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/camera/ImageUploadVM.kt index 056aeec..6e1a853 100644 --- a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/camera/ImageUploadVM.kt +++ b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/camera/ImageUploadVM.kt @@ -1,12 +1,15 @@ package com.featuremodule.homeImpl.camera +import com.featuremodule.core.navigation.NavCommand +import com.featuremodule.core.navigation.NavManager import com.featuremodule.core.ui.BaseVM +import com.featuremodule.homeImpl.InternalRoutes import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject @HiltViewModel internal class ImageUploadVM @Inject constructor( - + private val navManager: NavManager, ) : BaseVM() { override fun initialState() = State() @@ -14,6 +17,11 @@ internal class ImageUploadVM @Inject constructor( when (event) { is Event.PhotoTaken -> setState { copy(image = event.bitmap) } is Event.ImagePicked -> setState { copy(image = event.uri) } + Event.OpenInAppCamera -> launch { + navManager.navigate( + NavCommand.Forward(InternalRoutes.TakePhotoDestination.constructRoute()), + ) + } } } } From 95c0d342c6b5d843e8962c9595e086432a62bbf1 Mon Sep 17 00:00:00 2001 From: retanar Date: Tue, 26 Nov 2024 15:51:08 +0200 Subject: [PATCH 04/10] Added camera preview to the screen --- feature/homeImpl/build.gradle.kts | 1 + .../homeImpl/camera/TakePhotoScreen.kt | 69 +++++++++++++++++++ gradle/libs.versions.toml | 7 ++ 3 files changed, 77 insertions(+) diff --git a/feature/homeImpl/build.gradle.kts b/feature/homeImpl/build.gradle.kts index 91ab193..4314ace 100644 --- a/feature/homeImpl/build.gradle.kts +++ b/feature/homeImpl/build.gradle.kts @@ -11,6 +11,7 @@ dependencies { implementation(projects.feature.featureAApi) implementation(libs.bundles.exoplayer) + implementation(libs.bundles.camerax) implementation(libs.glide.compose) } diff --git a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/camera/TakePhotoScreen.kt b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/camera/TakePhotoScreen.kt index e38707b..e956ce7 100644 --- a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/camera/TakePhotoScreen.kt +++ b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/camera/TakePhotoScreen.kt @@ -1,7 +1,76 @@ package com.featuremodule.homeImpl.camera +import android.content.Context +import android.util.Rational +import androidx.camera.core.CameraSelector +import androidx.camera.core.ImageCapture +import androidx.camera.core.Preview +import androidx.camera.lifecycle.ProcessCameraProvider +import androidx.camera.view.PreviewView +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +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 @Composable internal fun TakePhotoScreen() { + val context = LocalContext.current + val lifecycleOwner = LocalLifecycleOwner.current + val previewView = remember { + PreviewView(context).apply { + scaleType = PreviewView.ScaleType.FILL_CENTER + } + } + val imageCapture = remember { + ImageCapture.Builder().build().apply { + setCropAspectRatio(Rational(1, 1)) + } + } + + LaunchedEffect(context, lifecycleOwner) { + setupCameraSettings( + context = context, + lifecycleOwner = lifecycleOwner, + previewView = previewView, + imageCapture = imageCapture, + ) + } + + Box { + AndroidView( + factory = { previewView }, + Modifier + .aspectRatio(1f) + .fillMaxSize() + .align(Alignment.Center), + ) + } +} + +private fun setupCameraSettings( + context: Context, + lifecycleOwner: LifecycleOwner, + previewView: PreviewView, + imageCapture: ImageCapture, +) { + ProcessCameraProvider.getInstance(context).run { + addListener( + { + val cameraSelector = CameraSelector.Builder().build() + val preview = Preview.Builder().build() + preview.surfaceProvider = previewView.surfaceProvider + get().bindToLifecycle(lifecycleOwner, cameraSelector, preview, imageCapture) + }, + ContextCompat.getMainExecutor(context), + ) + } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4eb3191..b240345 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -23,6 +23,7 @@ collections-immutable = "0.3.7" glide-compose = "1.0.0-beta01" leakcanary = "2.14" media3 = "1.4.1" +camerax = "1.4.0" # Versions used for android{} setup sdk-compile = "34" @@ -72,6 +73,11 @@ glide-compose = { module = "com.github.bumptech.glide:compose", version.ref = "g media3-exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "media3" } media3-ui = { module = "androidx.media3:media3-ui", version.ref = "media3" } +camerax-core = { module = "androidx.camera:camera-core", version.ref = "camerax" } +camerax-camera2 = { module = "androidx.camera:camera-camera2", version.ref = "camerax" } +camerax-view = { module = "androidx.camera:camera-view", version.ref = "camerax" } +camerax-lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref = "camerax" } + # Testing junit = { module = "junit:junit", version.ref = "junit" } androidx-junit = { module = "androidx.test.ext:junit", version.ref = "junit-androidx" } @@ -88,6 +94,7 @@ compose = ["androidx-lifecycle-runtime-compose", "compose-ui", "compose-ui-graph "compose-ui-tooling-preview", "compose-material3", "compose-runtime"] network = ["retrofit", "retrofit-converter-moshi", "moshi"] exoplayer = ["media3-exoplayer", "media3-ui"] +camerax = ["camerax-core", "camerax-camera2", "camerax-view", "camerax-lifecycle"] [plugins] kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } From e56d0ea949da5af80c10fab7ab53aadfe67879fe Mon Sep 17 00:00:00 2001 From: retanar Date: Tue, 26 Nov 2024 16:10:28 +0200 Subject: [PATCH 05/10] Changed package --- .../src/main/java/com/featuremodule/homeImpl/HomeGraphEntry.kt | 2 +- .../homeImpl/{camera => imageUpload}/ImageUploadContract.kt | 2 +- .../homeImpl/{camera => imageUpload}/ImageUploadScreen.kt | 2 +- .../homeImpl/{camera => imageUpload}/ImageUploadVM.kt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) rename feature/homeImpl/src/main/java/com/featuremodule/homeImpl/{camera => imageUpload}/ImageUploadContract.kt (89%) rename feature/homeImpl/src/main/java/com/featuremodule/homeImpl/{camera => imageUpload}/ImageUploadScreen.kt (98%) rename feature/homeImpl/src/main/java/com/featuremodule/homeImpl/{camera => imageUpload}/ImageUploadVM.kt (95%) diff --git a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/HomeGraphEntry.kt b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/HomeGraphEntry.kt index 7a88b96..e499dd6 100644 --- a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/HomeGraphEntry.kt +++ b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/HomeGraphEntry.kt @@ -4,7 +4,7 @@ import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable import com.featuremodule.core.navigation.HIDE_NAV_BAR import com.featuremodule.homeApi.HomeDestination -import com.featuremodule.homeImpl.camera.ImageUploadScreen +import com.featuremodule.homeImpl.imageUpload.ImageUploadScreen import com.featuremodule.homeImpl.camera.TakePhotoScreen import com.featuremodule.homeImpl.exoplayer.ExoplayerScreen import com.featuremodule.homeImpl.ui.HomeScreen diff --git a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/camera/ImageUploadContract.kt b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/imageUpload/ImageUploadContract.kt similarity index 89% rename from feature/homeImpl/src/main/java/com/featuremodule/homeImpl/camera/ImageUploadContract.kt rename to feature/homeImpl/src/main/java/com/featuremodule/homeImpl/imageUpload/ImageUploadContract.kt index 0f97918..2d53add 100644 --- a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/camera/ImageUploadContract.kt +++ b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/imageUpload/ImageUploadContract.kt @@ -1,4 +1,4 @@ -package com.featuremodule.homeImpl.camera +package com.featuremodule.homeImpl.imageUpload import android.graphics.Bitmap import android.net.Uri diff --git a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/camera/ImageUploadScreen.kt b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/imageUpload/ImageUploadScreen.kt similarity index 98% rename from feature/homeImpl/src/main/java/com/featuremodule/homeImpl/camera/ImageUploadScreen.kt rename to feature/homeImpl/src/main/java/com/featuremodule/homeImpl/imageUpload/ImageUploadScreen.kt index 5753fe6..f88927d 100644 --- a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/camera/ImageUploadScreen.kt +++ b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/imageUpload/ImageUploadScreen.kt @@ -1,4 +1,4 @@ -package com.featuremodule.homeImpl.camera +package com.featuremodule.homeImpl.imageUpload import android.Manifest import android.content.pm.PackageManager diff --git a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/camera/ImageUploadVM.kt b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/imageUpload/ImageUploadVM.kt similarity index 95% rename from feature/homeImpl/src/main/java/com/featuremodule/homeImpl/camera/ImageUploadVM.kt rename to feature/homeImpl/src/main/java/com/featuremodule/homeImpl/imageUpload/ImageUploadVM.kt index 6e1a853..8f1fea8 100644 --- a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/camera/ImageUploadVM.kt +++ b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/imageUpload/ImageUploadVM.kt @@ -1,4 +1,4 @@ -package com.featuremodule.homeImpl.camera +package com.featuremodule.homeImpl.imageUpload import com.featuremodule.core.navigation.NavCommand import com.featuremodule.core.navigation.NavManager From 24646b64af9a77209dada7cbab38704c2117345c Mon Sep 17 00:00:00 2001 From: retanar Date: Wed, 27 Nov 2024 13:34:49 +0200 Subject: [PATCH 06/10] Added taking photo ability --- .../featuremodule/homeImpl/HomeGraphEntry.kt | 13 +++++-- .../homeImpl/camera/TakePhotoContract.kt | 11 ++++++ .../homeImpl/camera/TakePhotoScreen.kt | 34 ++++++++++++++++++- .../homeImpl/camera/TakePhotoVM.kt | 27 +++++++++++++++ .../homeImpl/imageUpload/ImageUploadScreen.kt | 13 ++++++- 5 files changed, 93 insertions(+), 5 deletions(-) create mode 100644 feature/homeImpl/src/main/java/com/featuremodule/homeImpl/camera/TakePhotoContract.kt create mode 100644 feature/homeImpl/src/main/java/com/featuremodule/homeImpl/camera/TakePhotoVM.kt diff --git a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/HomeGraphEntry.kt b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/HomeGraphEntry.kt index e499dd6..62b9464 100644 --- a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/HomeGraphEntry.kt +++ b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/HomeGraphEntry.kt @@ -1,12 +1,15 @@ package com.featuremodule.homeImpl +import android.graphics.Bitmap +import androidx.compose.runtime.getValue +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable import com.featuremodule.core.navigation.HIDE_NAV_BAR import com.featuremodule.homeApi.HomeDestination -import com.featuremodule.homeImpl.imageUpload.ImageUploadScreen import com.featuremodule.homeImpl.camera.TakePhotoScreen import com.featuremodule.homeImpl.exoplayer.ExoplayerScreen +import com.featuremodule.homeImpl.imageUpload.ImageUploadScreen import com.featuremodule.homeImpl.ui.HomeScreen fun NavGraphBuilder.registerHome() { @@ -18,8 +21,11 @@ fun NavGraphBuilder.registerHome() { ExoplayerScreen() } - composable(InternalRoutes.ImageUploadDestination.ROUTE) { - ImageUploadScreen() + composable(InternalRoutes.ImageUploadDestination.ROUTE) { backStack -> + val bitmap by backStack.savedStateHandle + .getStateFlow(InternalRoutes.ImageUploadDestination.BITMAP_POP_ARG, null) + .collectAsStateWithLifecycle() + ImageUploadScreen(returnedBitmap = bitmap) } composable(InternalRoutes.TakePhotoDestination.ROUTE) { @@ -36,6 +42,7 @@ internal class InternalRoutes { object ImageUploadDestination { const val ROUTE = "image_upload" + const val BITMAP_POP_ARG = "bitmap" fun constructRoute() = ROUTE } diff --git a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/camera/TakePhotoContract.kt b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/camera/TakePhotoContract.kt new file mode 100644 index 0000000..6beeeaf --- /dev/null +++ b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/camera/TakePhotoContract.kt @@ -0,0 +1,11 @@ +package com.featuremodule.homeImpl.camera + +import android.graphics.Bitmap +import com.featuremodule.core.ui.UiEvent +import com.featuremodule.core.ui.UiState + +internal class State : UiState + +internal sealed interface Event : UiEvent { + data class CaptureSuccess(val bitmap: Bitmap) : Event +} diff --git a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/camera/TakePhotoScreen.kt b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/camera/TakePhotoScreen.kt index e956ce7..76c4d4c 100644 --- a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/camera/TakePhotoScreen.kt +++ b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/camera/TakePhotoScreen.kt @@ -4,12 +4,19 @@ import android.content.Context import android.util.Rational import androidx.camera.core.CameraSelector import androidx.camera.core.ImageCapture +import androidx.camera.core.ImageCapture.OnImageCapturedCallback +import androidx.camera.core.ImageProxy import androidx.camera.core.Preview import androidx.camera.lifecycle.ProcessCameraProvider import androidx.camera.view.PreviewView +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember @@ -17,12 +24,14 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import androidx.core.content.ContextCompat +import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.LifecycleOwner @Composable -internal fun TakePhotoScreen() { +internal fun TakePhotoScreen(viewModel: TakePhotoVM = hiltViewModel()) { val context = LocalContext.current val lifecycleOwner = LocalLifecycleOwner.current val previewView = remember { @@ -53,6 +62,29 @@ internal fun TakePhotoScreen() { .fillMaxSize() .align(Alignment.Center), ) + + IconButton( + onClick = { + imageCapture.takePicture( + ContextCompat.getMainExecutor(context), + object : OnImageCapturedCallback() { + override fun onCaptureSuccess(image: ImageProxy) { + viewModel.postEvent(Event.CaptureSuccess(image.toBitmap())) + image.close() + } + }, + ) + }, + modifier = Modifier + .size(50.dp) + .align(Alignment.BottomCenter), + ) { + Box( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.primary, CircleShape), + ) + } } } diff --git a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/camera/TakePhotoVM.kt b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/camera/TakePhotoVM.kt new file mode 100644 index 0000000..926dac1 --- /dev/null +++ b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/camera/TakePhotoVM.kt @@ -0,0 +1,27 @@ +package com.featuremodule.homeImpl.camera + +import com.featuremodule.core.navigation.NavCommand +import com.featuremodule.core.navigation.NavManager +import com.featuremodule.core.ui.BaseVM +import com.featuremodule.homeImpl.InternalRoutes.ImageUploadDestination +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject + +@HiltViewModel +internal class TakePhotoVM @Inject constructor( + private val navManager: NavManager, +) : BaseVM() { + override fun initialState() = State() + + override fun handleEvent(event: Event) { + when (event) { + is Event.CaptureSuccess -> launch { + navManager.navigate( + NavCommand.PopBackWithArguments( + mapOf(ImageUploadDestination.BITMAP_POP_ARG to event.bitmap), + ), + ) + } + } + } +} diff --git a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/imageUpload/ImageUploadScreen.kt b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/imageUpload/ImageUploadScreen.kt index f88927d..8c909b9 100644 --- a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/imageUpload/ImageUploadScreen.kt +++ b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/imageUpload/ImageUploadScreen.kt @@ -2,6 +2,7 @@ package com.featuremodule.homeImpl.imageUpload import android.Manifest import android.content.pm.PackageManager +import android.graphics.Bitmap import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.PickVisualMediaRequest import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia @@ -17,6 +18,7 @@ import androidx.compose.foundation.layout.width import androidx.compose.material3.Button import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -29,7 +31,10 @@ import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi import com.bumptech.glide.integration.compose.GlideImage @Composable -internal fun ImageUploadScreen(viewModel: ImageUploadVM = hiltViewModel()) { +internal fun ImageUploadScreen( + returnedBitmap: Bitmap?, + viewModel: ImageUploadVM = hiltViewModel() +) { val context = LocalContext.current val state by viewModel.state.collectAsStateWithLifecycle() @@ -48,6 +53,12 @@ internal fun ImageUploadScreen(viewModel: ImageUploadVM = hiltViewModel()) { } } + LaunchedEffect(returnedBitmap) { + returnedBitmap?.let { + viewModel.postEvent(Event.PhotoTaken(it)) + } + } + ImageUploadScreen( state = state, launchPhotoTaker = { launchSystemPhotoTaker.launch() }, From 63a8638762c6c0a0345fdcab0632d9abcbc76c76 Mon Sep 17 00:00:00 2001 From: retanar Date: Fri, 29 Nov 2024 11:26:54 +0200 Subject: [PATCH 07/10] Replaced CameraProvider with CameraController, fixed some UI, added image rotation for proper orientation --- .../homeImpl/camera/TakePhotoContract.kt | 2 +- .../homeImpl/camera/TakePhotoScreen.kt | 74 ++++++++----------- .../homeImpl/camera/TakePhotoVM.kt | 21 +++++- .../homeImpl/imageUpload/ImageUploadScreen.kt | 20 ++++- 4 files changed, 70 insertions(+), 47 deletions(-) diff --git a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/camera/TakePhotoContract.kt b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/camera/TakePhotoContract.kt index 6beeeaf..680b5da 100644 --- a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/camera/TakePhotoContract.kt +++ b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/camera/TakePhotoContract.kt @@ -7,5 +7,5 @@ import com.featuremodule.core.ui.UiState internal class State : UiState internal sealed interface Event : UiEvent { - data class CaptureSuccess(val bitmap: Bitmap) : Event + data class CaptureSuccess(val bitmap: Bitmap, val rotation: Int) : Event } diff --git a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/camera/TakePhotoScreen.kt b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/camera/TakePhotoScreen.kt index 76c4d4c..09a3185 100644 --- a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/camera/TakePhotoScreen.kt +++ b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/camera/TakePhotoScreen.kt @@ -1,19 +1,20 @@ package com.featuremodule.homeImpl.camera -import android.content.Context -import android.util.Rational import androidx.camera.core.CameraSelector -import androidx.camera.core.ImageCapture import androidx.camera.core.ImageCapture.OnImageCapturedCallback import androidx.camera.core.ImageProxy -import androidx.camera.core.Preview -import androidx.camera.lifecycle.ProcessCameraProvider +import androidx.camera.view.CameraController +import androidx.camera.view.LifecycleCameraController import androidx.camera.view.PreviewView import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme @@ -22,13 +23,13 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import androidx.core.content.ContextCompat import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.LifecycleOwner @Composable internal fun TakePhotoScreen(viewModel: TakePhotoVM = hiltViewModel()) { @@ -39,45 +40,53 @@ internal fun TakePhotoScreen(viewModel: TakePhotoVM = hiltViewModel()) { scaleType = PreviewView.ScaleType.FILL_CENTER } } - val imageCapture = remember { - ImageCapture.Builder().build().apply { - setCropAspectRatio(Rational(1, 1)) + val cameraController = remember { + LifecycleCameraController(context).apply { + setEnabledUseCases(CameraController.IMAGE_CAPTURE) + bindToLifecycle(lifecycleOwner) + cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA } } - LaunchedEffect(context, lifecycleOwner) { - setupCameraSettings( - context = context, - lifecycleOwner = lifecycleOwner, - previewView = previewView, - imageCapture = imageCapture, - ) + LaunchedEffect(previewView, cameraController) { + previewView.controller = cameraController } - Box { + Box( + modifier = Modifier + .fillMaxSize() + .background(Color.Black) + .windowInsetsPadding(WindowInsets.navigationBars), + ) { AndroidView( factory = { previewView }, Modifier + .align(Alignment.Center) .aspectRatio(1f) - .fillMaxSize() - .align(Alignment.Center), + .fillMaxSize(), ) IconButton( onClick = { - imageCapture.takePicture( + cameraController.takePicture( ContextCompat.getMainExecutor(context), object : OnImageCapturedCallback() { override fun onCaptureSuccess(image: ImageProxy) { - viewModel.postEvent(Event.CaptureSuccess(image.toBitmap())) + viewModel.postEvent( + Event.CaptureSuccess( + image.toBitmap(), + image.imageInfo.rotationDegrees, + ), + ) image.close() } }, ) }, modifier = Modifier - .size(50.dp) - .align(Alignment.BottomCenter), + .align(Alignment.BottomCenter) + .padding(bottom = 16.dp) + .size(50.dp), ) { Box( modifier = Modifier @@ -87,22 +96,3 @@ internal fun TakePhotoScreen(viewModel: TakePhotoVM = hiltViewModel()) { } } } - -private fun setupCameraSettings( - context: Context, - lifecycleOwner: LifecycleOwner, - previewView: PreviewView, - imageCapture: ImageCapture, -) { - ProcessCameraProvider.getInstance(context).run { - addListener( - { - val cameraSelector = CameraSelector.Builder().build() - val preview = Preview.Builder().build() - preview.surfaceProvider = previewView.surfaceProvider - get().bindToLifecycle(lifecycleOwner, cameraSelector, preview, imageCapture) - }, - ContextCompat.getMainExecutor(context), - ) - } -} diff --git a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/camera/TakePhotoVM.kt b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/camera/TakePhotoVM.kt index 926dac1..7cfe96c 100644 --- a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/camera/TakePhotoVM.kt +++ b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/camera/TakePhotoVM.kt @@ -1,5 +1,7 @@ package com.featuremodule.homeImpl.camera +import android.graphics.Bitmap +import android.graphics.Matrix import com.featuremodule.core.navigation.NavCommand import com.featuremodule.core.navigation.NavManager import com.featuremodule.core.ui.BaseVM @@ -16,12 +18,29 @@ internal class TakePhotoVM @Inject constructor( override fun handleEvent(event: Event) { when (event) { is Event.CaptureSuccess -> launch { + val rotatedBitmap = rotateBitmap(event.bitmap, event.rotation) navManager.navigate( NavCommand.PopBackWithArguments( - mapOf(ImageUploadDestination.BITMAP_POP_ARG to event.bitmap), + mapOf(ImageUploadDestination.BITMAP_POP_ARG to rotatedBitmap), ), ) } } } + + // Because image is not rotated by default, it only has rotation value in EXIF + private fun rotateBitmap(bitmap: Bitmap, rotation: Int): Bitmap { + val matrix = Matrix().apply { + postRotate(rotation.toFloat()) + } + return Bitmap.createBitmap( + bitmap, + 0, + 0, + bitmap.width, + bitmap.height, + matrix, + true, + ) + } } diff --git a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/imageUpload/ImageUploadScreen.kt b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/imageUpload/ImageUploadScreen.kt index 8c909b9..885a93f 100644 --- a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/imageUpload/ImageUploadScreen.kt +++ b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/imageUpload/ImageUploadScreen.kt @@ -13,8 +13,11 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Button import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -22,6 +25,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import androidx.core.content.ContextCompat @@ -87,12 +91,22 @@ private fun ImageUploadScreen( launchImagePicker: () -> Unit, launchCamera: () -> Unit, ) { - Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + Box( + modifier = Modifier + .fillMaxSize() + .verticalScroll( + rememberScrollState(), + ), + contentAlignment = Alignment.Center, + ) { Column(modifier = Modifier.width(200.dp)) { GlideImage( model = state.image, contentDescription = null, - modifier = Modifier.size(200.dp), + modifier = Modifier + .padding(vertical = 16.dp) + .size(200.dp), + contentScale = ContentScale.Crop, ) @Composable @@ -107,7 +121,7 @@ private fun ImageUploadScreen( GenericButton(text = "Open camera app") { launchPhotoTaker() } GenericButton(text = "Open image picker") { launchImagePicker() } - GenericButton(text = "Open custom camera") { launchCamera() } + GenericButton(text = "Open in-app camera") { launchCamera() } } } } From fdd6a810612a092aa3ddfd0e208d649c7361297a Mon Sep 17 00:00:00 2001 From: retanar Date: Tue, 3 Dec 2024 10:26:15 +0200 Subject: [PATCH 08/10] Error handling --- .../homeImpl/imageUpload/ImageUploadScreen.kt | 128 +++++++++++++++--- 1 file changed, 109 insertions(+), 19 deletions(-) diff --git a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/imageUpload/ImageUploadScreen.kt b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/imageUpload/ImageUploadScreen.kt index 885a93f..ef2ff83 100644 --- a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/imageUpload/ImageUploadScreen.kt +++ b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/imageUpload/ImageUploadScreen.kt @@ -1,6 +1,7 @@ package com.featuremodule.homeImpl.imageUpload import android.Manifest +import android.content.ActivityNotFoundException import android.content.pm.PackageManager import android.graphics.Bitmap import androidx.activity.compose.rememberLauncherForActivityResult @@ -11,23 +12,32 @@ import androidx.activity.result.contract.ActivityResultContracts.TakePicturePrev import androidx.activity.result.launch import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Button +import androidx.compose.material3.Card import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog import androidx.core.content.ContextCompat import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -41,6 +51,8 @@ internal fun ImageUploadScreen( ) { val context = LocalContext.current val state by viewModel.state.collectAsStateWithLifecycle() + var permissionNotGrantedVisibility by remember { mutableStateOf(false) } + var cameraNotFoundVisibility by remember { mutableStateOf(false) } val launchSystemPhotoTaker = rememberLauncherForActivityResult(TakePicturePreview()) { bitmap -> bitmap?.let { viewModel.postEvent(Event.PhotoTaken(it)) } @@ -53,7 +65,7 @@ internal fun ImageUploadScreen( if (isGranted) { viewModel.postEvent(Event.OpenInAppCamera) } else { - // TODO("Show dialog for refused request") + permissionNotGrantedVisibility = true } } @@ -63,24 +75,50 @@ internal fun ImageUploadScreen( } } - ImageUploadScreen( - state = state, - launchPhotoTaker = { launchSystemPhotoTaker.launch() }, - launchImagePicker = { - launchImagePicker.launch(PickVisualMediaRequest(PickVisualMedia.ImageOnly)) - }, - launchCamera = { - if (ContextCompat.checkSelfPermission( - context, - Manifest.permission.CAMERA, - ) == PackageManager.PERMISSION_DENIED - ) { - launchCameraPermissionRequest.launch(Manifest.permission.CAMERA) - } else { - viewModel.postEvent(Event.OpenInAppCamera) - } - }, - ) + Box { + ImageUploadScreen( + state = state, + launchPhotoTaker = { + if (ContextCompat.checkSelfPermission( + context, + Manifest.permission.CAMERA, + ) == PackageManager.PERMISSION_DENIED + ) { + launchCameraPermissionRequest.launch(Manifest.permission.CAMERA) + } else { + try { + launchSystemPhotoTaker.launch() + } catch (e: ActivityNotFoundException) { + cameraNotFoundVisibility = true + } + } + }, + launchImagePicker = { + launchImagePicker.launch(PickVisualMediaRequest(PickVisualMedia.ImageOnly)) + }, + launchCamera = { + if (ContextCompat.checkSelfPermission( + context, + Manifest.permission.CAMERA, + ) == PackageManager.PERMISSION_DENIED + ) { + launchCameraPermissionRequest.launch(Manifest.permission.CAMERA) + } else { + viewModel.postEvent(Event.OpenInAppCamera) + } + }, + ) + + PermissionNotGrantedDialog( + isVisible = permissionNotGrantedVisibility, + onDismiss = { permissionNotGrantedVisibility = false }, + ) + + CameraNotFoundDialog( + isVisible = cameraNotFoundVisibility, + onDismiss = { cameraNotFoundVisibility = false }, + ) + } } @OptIn(ExperimentalGlideComposeApi::class) @@ -125,3 +163,55 @@ private fun ImageUploadScreen( } } } + +@Composable +private fun PermissionNotGrantedDialog(isVisible: Boolean, onDismiss: () -> Unit) { + if (!isVisible) return + + Dialog(onDismissRequest = onDismiss) { + Card { + Column( + modifier = Modifier + .padding(16.dp) + .width(IntrinsicSize.Max), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + text = "Permission was not granted", + fontWeight = FontWeight.SemiBold, + modifier = Modifier.fillMaxWidth(), + ) + Spacer(modifier = Modifier.height(16.dp)) + Button(onClick = onDismiss, modifier = Modifier.fillMaxWidth()) { + Text(text = "Close") + } + } + } + } +} + +@Composable +private fun CameraNotFoundDialog(isVisible: Boolean, onDismiss: () -> Unit) { + if (!isVisible) return + + Dialog(onDismissRequest = onDismiss) { + Card { + Column( + modifier = Modifier + .padding(16.dp) + .width(IntrinsicSize.Max), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + text = "Camera app was not found", + fontWeight = FontWeight.SemiBold, + modifier = Modifier.fillMaxWidth(), + ) + Spacer(modifier = Modifier.height(16.dp)) + Button(onClick = onDismiss, modifier = Modifier.fillMaxWidth()) { + Text(text = "Close") + } + } + } + } +} From ccce690d48c765c843c8c9c0e3e2044f591856ea Mon Sep 17 00:00:00 2001 From: retanar Date: Tue, 3 Dec 2024 11:36:34 +0200 Subject: [PATCH 09/10] Some bug fixing --- .../homeImpl/camera/TakePhotoScreen.kt | 30 ++++++++++--------- .../homeImpl/imageUpload/ImageUploadScreen.kt | 28 ++++++++++++----- 2 files changed, 36 insertions(+), 22 deletions(-) diff --git a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/camera/TakePhotoScreen.kt b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/camera/TakePhotoScreen.kt index 09a3185..d9d81f4 100644 --- a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/camera/TakePhotoScreen.kt +++ b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/camera/TakePhotoScreen.kt @@ -68,20 +68,22 @@ internal fun TakePhotoScreen(viewModel: TakePhotoVM = hiltViewModel()) { IconButton( onClick = { - cameraController.takePicture( - ContextCompat.getMainExecutor(context), - object : OnImageCapturedCallback() { - override fun onCaptureSuccess(image: ImageProxy) { - viewModel.postEvent( - Event.CaptureSuccess( - image.toBitmap(), - image.imageInfo.rotationDegrees, - ), - ) - image.close() - } - }, - ) + runCatching { + cameraController.takePicture( + ContextCompat.getMainExecutor(context), + object : OnImageCapturedCallback() { + override fun onCaptureSuccess(image: ImageProxy) { + viewModel.postEvent( + Event.CaptureSuccess( + image.toBitmap(), + image.imageInfo.rotationDegrees, + ), + ) + image.close() + } + }, + ) + } }, modifier = Modifier .align(Alignment.BottomCenter) diff --git a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/imageUpload/ImageUploadScreen.kt b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/imageUpload/ImageUploadScreen.kt index ef2ff83..40fc8c4 100644 --- a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/imageUpload/ImageUploadScreen.kt +++ b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/imageUpload/ImageUploadScreen.kt @@ -54,13 +54,25 @@ internal fun ImageUploadScreen( var permissionNotGrantedVisibility by remember { mutableStateOf(false) } var cameraNotFoundVisibility by remember { mutableStateOf(false) } - val launchSystemPhotoTaker = rememberLauncherForActivityResult(TakePicturePreview()) { bitmap -> + val launchSystemCamera = rememberLauncherForActivityResult(TakePicturePreview()) { bitmap -> bitmap?.let { viewModel.postEvent(Event.PhotoTaken(it)) } } + val launchSystemCameraPermissionRequest = + rememberLauncherForActivityResult(RequestPermission()) { isGranted -> + if (isGranted) { + try { + launchSystemCamera.launch() + } catch (e: ActivityNotFoundException) { + cameraNotFoundVisibility = true + } + } else { + permissionNotGrantedVisibility = true + } + } val launchImagePicker = rememberLauncherForActivityResult(PickVisualMedia()) { uri -> uri?.let { viewModel.postEvent(Event.ImagePicked(it)) } } - val launchCameraPermissionRequest = + val launchInAppCameraPermissionRequest = rememberLauncherForActivityResult(RequestPermission()) { isGranted -> if (isGranted) { viewModel.postEvent(Event.OpenInAppCamera) @@ -78,16 +90,16 @@ internal fun ImageUploadScreen( Box { ImageUploadScreen( state = state, - launchPhotoTaker = { + launchSystemCamera = { if (ContextCompat.checkSelfPermission( context, Manifest.permission.CAMERA, ) == PackageManager.PERMISSION_DENIED ) { - launchCameraPermissionRequest.launch(Manifest.permission.CAMERA) + launchSystemCameraPermissionRequest.launch(Manifest.permission.CAMERA) } else { try { - launchSystemPhotoTaker.launch() + launchSystemCamera.launch() } catch (e: ActivityNotFoundException) { cameraNotFoundVisibility = true } @@ -102,7 +114,7 @@ internal fun ImageUploadScreen( Manifest.permission.CAMERA, ) == PackageManager.PERMISSION_DENIED ) { - launchCameraPermissionRequest.launch(Manifest.permission.CAMERA) + launchInAppCameraPermissionRequest.launch(Manifest.permission.CAMERA) } else { viewModel.postEvent(Event.OpenInAppCamera) } @@ -125,7 +137,7 @@ internal fun ImageUploadScreen( @Composable private fun ImageUploadScreen( state: State, - launchPhotoTaker: () -> Unit, + launchSystemCamera: () -> Unit, launchImagePicker: () -> Unit, launchCamera: () -> Unit, ) { @@ -157,7 +169,7 @@ private fun ImageUploadScreen( } } - GenericButton(text = "Open camera app") { launchPhotoTaker() } + GenericButton(text = "Open camera app") { launchSystemCamera() } GenericButton(text = "Open image picker") { launchImagePicker() } GenericButton(text = "Open in-app camera") { launchCamera() } } From 78b26ade04b13b8147f6511663ed19f0ab8551ea Mon Sep 17 00:00:00 2001 From: retanar Date: Tue, 3 Dec 2024 11:44:07 +0200 Subject: [PATCH 10/10] Linters formatting --- .../homeImpl/imageUpload/ImageUploadScreen.kt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/imageUpload/ImageUploadScreen.kt b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/imageUpload/ImageUploadScreen.kt index 40fc8c4..93eb93c 100644 --- a/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/imageUpload/ImageUploadScreen.kt +++ b/feature/homeImpl/src/main/java/com/featuremodule/homeImpl/imageUpload/ImageUploadScreen.kt @@ -44,10 +44,11 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi import com.bumptech.glide.integration.compose.GlideImage +@Suppress("LongMethod") @Composable internal fun ImageUploadScreen( returnedBitmap: Bitmap?, - viewModel: ImageUploadVM = hiltViewModel() + viewModel: ImageUploadVM = hiltViewModel(), ) { val context = LocalContext.current val state by viewModel.state.collectAsStateWithLifecycle() @@ -62,7 +63,7 @@ internal fun ImageUploadScreen( if (isGranted) { try { launchSystemCamera.launch() - } catch (e: ActivityNotFoundException) { + } catch (_: ActivityNotFoundException) { cameraNotFoundVisibility = true } } else { @@ -100,7 +101,7 @@ internal fun ImageUploadScreen( } else { try { launchSystemCamera.launch() - } catch (e: ActivityNotFoundException) { + } catch (_: ActivityNotFoundException) { cameraNotFoundVisibility = true } }