From f70b8dcbb4d7c67d0d65683bdd0b29e42cdc7bb7 Mon Sep 17 00:00:00 2001 From: JI HUN LEE <51016231+easyhooon@users.noreply.github.com> Date: Thu, 1 Feb 2024 03:13:20 +0900 Subject: [PATCH 1/7] =?UTF-8?q?[FEAT]=20PhotoPicker=20=EB=A5=BC=20?= =?UTF-8?q?=ED=86=B5=ED=95=9C=20=EC=82=AC=EC=A7=84=20=EA=B0=A4=EB=9F=AC?= =?UTF-8?q?=EB=A6=AC=EC=97=90=EC=84=9C=20=EA=B0=80=EC=A0=B8=EC=98=A4?= =?UTF-8?q?=EB=8A=94=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 별다른 권한 필요 x --- .../android/feature/camera/CameraViewModel.kt | 8 --- .../feature/camera/UploadCheckScreen.kt | 50 ++++++++++++-- .../{UploadScreen.kt => UploadPhotoScreen.kt} | 69 ++++++++++++++----- .../feature/camera/UploadPhotoSideEffect.kt | 7 ++ .../feature/camera/UploadPhotoState.kt | 5 ++ .../feature/camera/UploadPhotoViewModel.kt | 34 +++++++++ .../camera/navigation/CameraNavigation.kt | 33 +++++++-- .../ilab/android/feature/main/MainScreen.kt | 1 + 8 files changed, 173 insertions(+), 34 deletions(-) delete mode 100644 feature/camera/src/main/kotlin/com/nexters/ilab/android/feature/camera/CameraViewModel.kt rename feature/camera/src/main/kotlin/com/nexters/ilab/android/feature/camera/{UploadScreen.kt => UploadPhotoScreen.kt} (75%) create mode 100644 feature/camera/src/main/kotlin/com/nexters/ilab/android/feature/camera/UploadPhotoSideEffect.kt create mode 100644 feature/camera/src/main/kotlin/com/nexters/ilab/android/feature/camera/UploadPhotoState.kt create mode 100644 feature/camera/src/main/kotlin/com/nexters/ilab/android/feature/camera/UploadPhotoViewModel.kt diff --git a/feature/camera/src/main/kotlin/com/nexters/ilab/android/feature/camera/CameraViewModel.kt b/feature/camera/src/main/kotlin/com/nexters/ilab/android/feature/camera/CameraViewModel.kt deleted file mode 100644 index 8d0f2c33..00000000 --- a/feature/camera/src/main/kotlin/com/nexters/ilab/android/feature/camera/CameraViewModel.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.nexters.ilab.android.feature.camera - -import androidx.lifecycle.ViewModel -import dagger.hilt.android.lifecycle.HiltViewModel -import javax.inject.Inject - -@HiltViewModel -class CameraViewModel @Inject constructor() : ViewModel() diff --git a/feature/camera/src/main/kotlin/com/nexters/ilab/android/feature/camera/UploadCheckScreen.kt b/feature/camera/src/main/kotlin/com/nexters/ilab/android/feature/camera/UploadCheckScreen.kt index fe4fa85c..5a557dcf 100644 --- a/feature/camera/src/main/kotlin/com/nexters/ilab/android/feature/camera/UploadCheckScreen.kt +++ b/feature/camera/src/main/kotlin/com/nexters/ilab/android/feature/camera/UploadCheckScreen.kt @@ -1,5 +1,8 @@ package com.nexters.ilab.android.feature.camera +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.PickVisualMediaRequest +import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -15,6 +18,8 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon 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 import androidx.compose.ui.draw.clip @@ -24,6 +29,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.nexters.ilab.android.core.designsystem.R import com.nexters.ilab.android.core.designsystem.theme.Contents1 import com.nexters.ilab.android.core.designsystem.theme.Contents2 @@ -42,18 +48,43 @@ import com.nexters.ilab.core.ui.component.TopAppBarNavigationType @Composable internal fun UploadCheckRoute( onBackClick: () -> Unit, - viewModel: CameraViewModel = hiltViewModel(), + viewModel: UploadPhotoViewModel = hiltViewModel(), ) { - UploadCheckScreen(onBackClick = onBackClick) + val uiState by viewModel.container.stateFlow.collectAsStateWithLifecycle() + + val singlePhotoPickerLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.PickVisualMedia(), + onResult = { uri -> viewModel.setSelectImageUri(uri.toString()) }, + ) + + LaunchedEffect(viewModel) { + viewModel.container.sideEffectFlow.collect { sideEffect -> + when (sideEffect) { + is UploadPhotoSideEffect.openPhotoPicker -> { + singlePhotoPickerLauncher.launch( + PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly), + ) + } + + is UploadPhotoSideEffect.openCamera -> {} + is UploadPhotoSideEffect.UploadPhotoSuccess -> {} + } + } + } + UploadCheckScreen( + uiState = uiState, + onBackClick = onBackClick, + ) } @Composable private fun UploadCheckScreen( + uiState: UploadPhotoState, onBackClick: () -> Unit, ) { Column { UploadCheckTopAppBar(onBackClick = onBackClick) - UploadCheckContent() + UploadCheckContent(uiState.selectedPhotoUri) } } @@ -73,7 +104,9 @@ private fun UploadCheckTopAppBar( } @Composable -private fun UploadCheckContent() { +private fun UploadCheckContent( + selectedPhotoUri: String, +) { Column( modifier = Modifier .fillMaxSize() @@ -93,7 +126,7 @@ private fun UploadCheckContent() { ) Spacer(modifier = Modifier.height(36.dp)) NetworkImage( - imageUrl = "https://picsum.photos/300/300", + imageUrl = selectedPhotoUri, contentDescription = "upload image", modifier = Modifier .fillMaxWidth() @@ -191,5 +224,10 @@ fun GuideRow( @DevicePreview @Composable fun UploadCheckScreenPreview() { - UploadCheckScreen(onBackClick = {}) + UploadCheckScreen( + uiState = UploadPhotoState( + selectedPhotoUri = "", + ), + onBackClick = {}, + ) } diff --git a/feature/camera/src/main/kotlin/com/nexters/ilab/android/feature/camera/UploadScreen.kt b/feature/camera/src/main/kotlin/com/nexters/ilab/android/feature/camera/UploadPhotoScreen.kt similarity index 75% rename from feature/camera/src/main/kotlin/com/nexters/ilab/android/feature/camera/UploadScreen.kt rename to feature/camera/src/main/kotlin/com/nexters/ilab/android/feature/camera/UploadPhotoScreen.kt index dbd1658e..92188f6f 100644 --- a/feature/camera/src/main/kotlin/com/nexters/ilab/android/feature/camera/UploadScreen.kt +++ b/feature/camera/src/main/kotlin/com/nexters/ilab/android/feature/camera/UploadPhotoScreen.kt @@ -1,5 +1,8 @@ package com.nexters.ilab.android.feature.camera +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.PickVisualMediaRequest +import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -14,6 +17,8 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape 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 import androidx.compose.ui.draw.clip @@ -24,6 +29,7 @@ import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.nexters.ilab.android.core.designsystem.R import com.nexters.ilab.android.core.designsystem.theme.Contents2 import com.nexters.ilab.android.core.designsystem.theme.PurpleBlue200 @@ -42,34 +48,62 @@ import kotlinx.collections.immutable.persistentListOf @Suppress("unused") @Composable -internal fun UploadRoute( +internal fun UploadPhotoRoute( onBackClick: () -> Unit, onNavigateToUploadCheck: () -> Unit, - viewModel: CameraViewModel = hiltViewModel(), + viewModel: UploadPhotoViewModel = hiltViewModel(), ) { - UploadScreen( + val uiState by viewModel.container.stateFlow.collectAsStateWithLifecycle() + + val singlePhotoPickerLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.PickVisualMedia(), + onResult = { uri -> viewModel.setSelectImageUri(uri.toString()) } + ) + + LaunchedEffect(viewModel) { + viewModel.container.sideEffectFlow.collect { sideEffect -> + when (sideEffect) { + is UploadPhotoSideEffect.openPhotoPicker -> { + singlePhotoPickerLauncher.launch( + PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly), + ) + } + is UploadPhotoSideEffect.openCamera -> {} + is UploadPhotoSideEffect.UploadPhotoSuccess -> onNavigateToUploadCheck() + } + } + } + + UploadPhotoScreen( + uiState = uiState, onBackClick = onBackClick, - onNavigateToUploadCheck = onNavigateToUploadCheck, + onPhotoPickerClick = viewModel::onPhotoPickerClick, + onCameraClick = viewModel::onCameraClick, ) } @Composable -internal fun UploadScreen( +internal fun UploadPhotoScreen( + uiState: UploadPhotoState, onBackClick: () -> Unit, - onNavigateToUploadCheck: () -> Unit, + onPhotoPickerClick: () -> Unit, + onCameraClick: () -> Unit, ) { Column( modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally, ) { - UploadTopAppBar(onBackClick = onBackClick) - UploadContent(onNavigateToUploadCheck = onNavigateToUploadCheck) + UploadPhotoTopAppBar(onBackClick = onBackClick) + UploadPhotoContent( + onPhotoPickerClick = onPhotoPickerClick, + onCameraClick = onCameraClick, + ) } } @Composable -private fun UploadTopAppBar( +private fun UploadPhotoTopAppBar( onBackClick: () -> Unit, ) { ILabTopAppBar( @@ -84,8 +118,9 @@ private fun UploadTopAppBar( } @Composable -private fun UploadContent( - onNavigateToUploadCheck: () -> Unit, +private fun UploadPhotoContent( + onPhotoPickerClick: () -> Unit, + onCameraClick: () -> Unit, ) { Column( modifier = Modifier @@ -141,7 +176,7 @@ private fun UploadContent( .padding(start = 4.dp, end = 4.dp, bottom = 18.dp), ) { ILabButton( - onClick = {}, + onClick = onPhotoPickerClick, modifier = Modifier .weight(1f) .height(60.dp) @@ -156,7 +191,7 @@ private fun UploadContent( }, ) ILabButton( - onClick = onNavigateToUploadCheck, + onClick = onCameraClick, modifier = Modifier .weight(1f) .height(60.dp) @@ -201,9 +236,11 @@ fun ImageRow(images: ImmutableList>) { @DevicePreview @Composable -fun UploadScreenPreview() { - UploadScreen( +fun UploadPhotoScreenPreview() { + UploadPhotoScreen( + uiState = UploadPhotoState(), onBackClick = {}, - onNavigateToUploadCheck = {}, + onPhotoPickerClick = {}, + onCameraClick = {}, ) } diff --git a/feature/camera/src/main/kotlin/com/nexters/ilab/android/feature/camera/UploadPhotoSideEffect.kt b/feature/camera/src/main/kotlin/com/nexters/ilab/android/feature/camera/UploadPhotoSideEffect.kt new file mode 100644 index 00000000..75d52766 --- /dev/null +++ b/feature/camera/src/main/kotlin/com/nexters/ilab/android/feature/camera/UploadPhotoSideEffect.kt @@ -0,0 +1,7 @@ +package com.nexters.ilab.android.feature.camera + +sealed interface UploadPhotoSideEffect { + data object openPhotoPicker : UploadPhotoSideEffect + data object openCamera : UploadPhotoSideEffect + data object UploadPhotoSuccess : UploadPhotoSideEffect +} diff --git a/feature/camera/src/main/kotlin/com/nexters/ilab/android/feature/camera/UploadPhotoState.kt b/feature/camera/src/main/kotlin/com/nexters/ilab/android/feature/camera/UploadPhotoState.kt new file mode 100644 index 00000000..c08ecb7c --- /dev/null +++ b/feature/camera/src/main/kotlin/com/nexters/ilab/android/feature/camera/UploadPhotoState.kt @@ -0,0 +1,5 @@ +package com.nexters.ilab.android.feature.camera + +data class UploadPhotoState( + val selectedPhotoUri: String = "", +) diff --git a/feature/camera/src/main/kotlin/com/nexters/ilab/android/feature/camera/UploadPhotoViewModel.kt b/feature/camera/src/main/kotlin/com/nexters/ilab/android/feature/camera/UploadPhotoViewModel.kt new file mode 100644 index 00000000..1f765248 --- /dev/null +++ b/feature/camera/src/main/kotlin/com/nexters/ilab/android/feature/camera/UploadPhotoViewModel.kt @@ -0,0 +1,34 @@ +package com.nexters.ilab.android.feature.camera + +import androidx.lifecycle.ViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import org.orbitmvi.orbit.ContainerHost +import org.orbitmvi.orbit.syntax.simple.intent +import org.orbitmvi.orbit.syntax.simple.postSideEffect +import org.orbitmvi.orbit.syntax.simple.reduce +import org.orbitmvi.orbit.viewmodel.container +import javax.inject.Inject + +@HiltViewModel +class UploadPhotoViewModel @Inject constructor() : ViewModel(), ContainerHost { + + override val container = container(UploadPhotoState()) + + fun onPhotoPickerClick() = intent { + postSideEffect(UploadPhotoSideEffect.openPhotoPicker) + } + + fun onCameraClick() = intent { + postSideEffect(UploadPhotoSideEffect.openCamera) + } + + fun setSelectImageUri(uri: String) = intent { + reduce { + state.copy( + selectedPhotoUri = uri, + ) + } + postSideEffect(UploadPhotoSideEffect.UploadPhotoSuccess) + } +} + diff --git a/feature/camera/src/main/kotlin/com/nexters/ilab/android/feature/camera/navigation/CameraNavigation.kt b/feature/camera/src/main/kotlin/com/nexters/ilab/android/feature/camera/navigation/CameraNavigation.kt index 76dd5e5b..aaec6f14 100644 --- a/feature/camera/src/main/kotlin/com/nexters/ilab/android/feature/camera/navigation/CameraNavigation.kt +++ b/feature/camera/src/main/kotlin/com/nexters/ilab/android/feature/camera/navigation/CameraNavigation.kt @@ -1,12 +1,19 @@ package com.nexters.ilab.android.feature.camera.navigation +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.ViewModel +import androidx.navigation.NavBackStackEntry import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavHostController import androidx.navigation.NavOptions import androidx.navigation.compose.composable import androidx.navigation.compose.navigation import com.nexters.ilab.android.feature.camera.UploadCheckRoute -import com.nexters.ilab.android.feature.camera.UploadRoute +import com.nexters.ilab.android.feature.camera.UploadPhotoRoute +import com.nexters.ilab.android.feature.camera.UploadPhotoViewModel const val CAMERA_ROUTE = "camera_route" const val UPLOAD_ROUTE = "upload_route" @@ -21,6 +28,7 @@ fun NavController.navigateToUploadCheck() { } fun NavGraphBuilder.cameraNavGraph( + navController: NavHostController, onBackClick: () -> Unit, onNavigateToUploadCheck: () -> Unit, ) { @@ -28,17 +36,34 @@ fun NavGraphBuilder.cameraNavGraph( startDestination = UPLOAD_ROUTE, route = CAMERA_ROUTE, ) { - composable(route = UPLOAD_ROUTE) { - UploadRoute( + composable(route = UPLOAD_ROUTE) { entry -> + val viewModel = entry.sharedViewModel(navController) + UploadPhotoRoute( onBackClick = onBackClick, onNavigateToUploadCheck = onNavigateToUploadCheck, + viewModel = viewModel, ) } - composable(route = UPLOAD_CHECK_ROUTE) { + composable(route = UPLOAD_CHECK_ROUTE) { entry -> + val viewModel = entry.sharedViewModel(navController) UploadCheckRoute( onBackClick = onBackClick, + viewModel = viewModel, ) } } } + +// https://youtu.be/h61Wqy3qcKg?si=OqctoATR5MGbypOW +@Composable +inline fun NavBackStackEntry.sharedViewModel( + navController: NavHostController, +): T { + val navGraphRoute = destination.parent?.route ?: return hiltViewModel() + val parentEntry = remember(this) { + navController.getBackStackEntry(navGraphRoute) + } + return hiltViewModel(parentEntry) +} + diff --git a/feature/main/src/main/kotlin/com/nexters/ilab/android/feature/main/MainScreen.kt b/feature/main/src/main/kotlin/com/nexters/ilab/android/feature/main/MainScreen.kt index f7f18d8b..b58ac006 100644 --- a/feature/main/src/main/kotlin/com/nexters/ilab/android/feature/main/MainScreen.kt +++ b/feature/main/src/main/kotlin/com/nexters/ilab/android/feature/main/MainScreen.kt @@ -83,6 +83,7 @@ internal fun MainScreen( ) cameraNavGraph( + navController = navigator.navController, onBackClick = navigator::popBackStackIfNotHome, onNavigateToUploadCheck = navigator::navigateToUploadCheck, ) From 79926b3a843975d36f0c7b10ad19e7eb12e44704 Mon Sep 17 00:00:00 2001 From: JI HUN LEE <51016231+easyhooon@users.noreply.github.com> Date: Thu, 1 Feb 2024 03:23:20 +0900 Subject: [PATCH 2/7] =?UTF-8?q?[FEAT]=20camera=20=EB=AA=A8=EB=93=88=20uplo?= =?UTF-8?q?adphoto=20=EB=AA=A8=EB=93=88=EB=A1=9C=20=EB=84=A4=EC=9D=B4?= =?UTF-8?q?=EB=B0=8D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 카메라만 사용하는 것이 아닌, 갤러리에 접근하여 사진을 가져오는 기능도 수행하기 때문에, 사진 업로드를 하는 기능을 가진 feature 이므로 모듈 이름을 변경 --- app/build.gradle.kts | 2 +- .../src/main/res/values/strings.xml | 4 ++-- feature/main/build.gradle.kts | 2 +- .../android/feature/main/MainNavController.kt | 6 +++--- .../ilab/android/feature/main/MainScreen.kt | 4 ++-- .../ilab/android/feature/main/MainTab.kt | 6 +++--- feature/{camera => uploadphoto}/.gitignore | 0 .../{camera => uploadphoto}/build.gradle.kts | 2 +- .../feature/uploadphoto}/UploadCheckScreen.kt | 2 +- .../feature/uploadphoto}/UploadPhotoScreen.kt | 2 +- .../uploadphoto}/UploadPhotoSideEffect.kt | 2 +- .../feature/uploadphoto}/UploadPhotoState.kt | 2 +- .../uploadphoto}/UploadPhotoViewModel.kt | 2 +- .../navigation/UploadPhotoNavigation.kt} | 18 +++++++++--------- settings.gradle.kts | 2 +- 15 files changed, 28 insertions(+), 28 deletions(-) rename feature/{camera => uploadphoto}/.gitignore (100%) rename feature/{camera => uploadphoto}/build.gradle.kts (80%) rename feature/{camera/src/main/kotlin/com/nexters/ilab/android/feature/camera => uploadphoto/src/main/kotlin/com/nexters/ilab/android/feature/uploadphoto}/UploadCheckScreen.kt (99%) rename feature/{camera/src/main/kotlin/com/nexters/ilab/android/feature/camera => uploadphoto/src/main/kotlin/com/nexters/ilab/android/feature/uploadphoto}/UploadPhotoScreen.kt (99%) rename feature/{camera/src/main/kotlin/com/nexters/ilab/android/feature/camera => uploadphoto/src/main/kotlin/com/nexters/ilab/android/feature/uploadphoto}/UploadPhotoSideEffect.kt (79%) rename feature/{camera/src/main/kotlin/com/nexters/ilab/android/feature/camera => uploadphoto/src/main/kotlin/com/nexters/ilab/android/feature/uploadphoto}/UploadPhotoState.kt (57%) rename feature/{camera/src/main/kotlin/com/nexters/ilab/android/feature/camera => uploadphoto/src/main/kotlin/com/nexters/ilab/android/feature/uploadphoto}/UploadPhotoViewModel.kt (95%) rename feature/{camera/src/main/kotlin/com/nexters/ilab/android/feature/camera/navigation/CameraNavigation.kt => uploadphoto/src/main/kotlin/com/nexters/ilab/android/feature/uploadphoto/navigation/UploadPhotoNavigation.kt} (78%) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index b04d3568..6f184913 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -30,7 +30,6 @@ dependencies { projects.core.datastore, projects.core.ui, - projects.feature.camera, projects.feature.home, projects.feature.intro, projects.feature.login, @@ -38,6 +37,7 @@ dependencies { projects.feature.navigator, projects.feature.mypage, projects.feature.setting, + projects.feature.uploadphoto, libs.androidx.activity.compose, libs.androidx.core, diff --git a/core/designsystem/src/main/res/values/strings.xml b/core/designsystem/src/main/res/values/strings.xml index a20c250e..95f1c8c1 100644 --- a/core/designsystem/src/main/res/values/strings.xml +++ b/core/designsystem/src/main/res/values/strings.xml @@ -1,12 +1,12 @@ - 카메라 + 사진 업로드 마이페이지 네트워크 연결이 원활하지 않습니다. 알 수 없는 오류가 발생하였습니다. - + 사진 가이드 가이드 확인 사진 찍기 diff --git a/feature/main/build.gradle.kts b/feature/main/build.gradle.kts index 893dc498..36d8b218 100644 --- a/feature/main/build.gradle.kts +++ b/feature/main/build.gradle.kts @@ -10,10 +10,10 @@ android { dependencies { implementations( - projects.feature.camera, projects.feature.home, projects.feature.mypage, projects.feature.setting, + projects.feature.uploadphoto, libs.androidx.core, libs.kotlinx.collections.immutable, diff --git a/feature/main/src/main/kotlin/com/nexters/ilab/android/feature/main/MainNavController.kt b/feature/main/src/main/kotlin/com/nexters/ilab/android/feature/main/MainNavController.kt index 0ceb7ccd..ff25dd3e 100644 --- a/feature/main/src/main/kotlin/com/nexters/ilab/android/feature/main/MainNavController.kt +++ b/feature/main/src/main/kotlin/com/nexters/ilab/android/feature/main/MainNavController.kt @@ -8,8 +8,8 @@ import androidx.navigation.NavHostController import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController import androidx.navigation.navOptions -import com.nexters.ilab.android.feature.camera.navigation.navigateToCamera -import com.nexters.ilab.android.feature.camera.navigation.navigateToUploadCheck +import com.nexters.ilab.android.feature.uploadphoto.navigation.navigateToUploadPhoto +import com.nexters.ilab.android.feature.uploadphoto.navigation.navigateToUploadCheck import com.nexters.ilab.android.feature.home.navigation.HOME_ROUTE import com.nexters.ilab.android.feature.home.navigation.navigateToHome import com.nexters.ilab.android.feature.mypage.navigation.navigateToMyPage @@ -40,7 +40,7 @@ internal class MainNavController( when (tab) { MainTab.HOME -> navController.navigateToHome(navOptions) - MainTab.CAMERA -> navController.navigateToCamera(navOptions) + MainTab.UPLOAD_PHOTO -> navController.navigateToUploadPhoto(navOptions) MainTab.MY_PAGE -> navController.navigateToMyPage(navOptions) } } diff --git a/feature/main/src/main/kotlin/com/nexters/ilab/android/feature/main/MainScreen.kt b/feature/main/src/main/kotlin/com/nexters/ilab/android/feature/main/MainScreen.kt index b58ac006..5b11f5c8 100644 --- a/feature/main/src/main/kotlin/com/nexters/ilab/android/feature/main/MainScreen.kt +++ b/feature/main/src/main/kotlin/com/nexters/ilab/android/feature/main/MainScreen.kt @@ -38,7 +38,7 @@ import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import androidx.navigation.compose.NavHost import com.nexters.ilab.android.core.designsystem.R -import com.nexters.ilab.android.feature.camera.navigation.cameraNavGraph +import com.nexters.ilab.android.feature.uploadphoto.navigation.uploadPhotoNavGraph import com.nexters.ilab.android.feature.home.navigation.homeNavGraph import com.nexters.ilab.android.feature.mypage.navigation.myPageNavGraph import com.nexters.ilab.android.feature.setting.navigation.settingNavGraph @@ -82,7 +82,7 @@ internal fun MainScreen( onShowErrorSnackBar = onShowErrorSnackBar, ) - cameraNavGraph( + uploadPhotoNavGraph( navController = navigator.navController, onBackClick = navigator::popBackStackIfNotHome, onNavigateToUploadCheck = navigator::navigateToUploadCheck, diff --git a/feature/main/src/main/kotlin/com/nexters/ilab/android/feature/main/MainTab.kt b/feature/main/src/main/kotlin/com/nexters/ilab/android/feature/main/MainTab.kt index 25e8e315..e88c1254 100644 --- a/feature/main/src/main/kotlin/com/nexters/ilab/android/feature/main/MainTab.kt +++ b/feature/main/src/main/kotlin/com/nexters/ilab/android/feature/main/MainTab.kt @@ -12,10 +12,10 @@ internal enum class MainTab( contentDescription = "홈", route = "home_route", ), - CAMERA( + UPLOAD_PHOTO( iconResId = R.drawable.ic_camera, - contentDescription = "카메라", - route = "camera_route", + contentDescription = "사진 업로드", + route = "upload_photo_route", ), MY_PAGE( iconResId = R.drawable.ic_my_page, diff --git a/feature/camera/.gitignore b/feature/uploadphoto/.gitignore similarity index 100% rename from feature/camera/.gitignore rename to feature/uploadphoto/.gitignore diff --git a/feature/camera/build.gradle.kts b/feature/uploadphoto/build.gradle.kts similarity index 80% rename from feature/camera/build.gradle.kts rename to feature/uploadphoto/build.gradle.kts index bf7e8b4c..c7345326 100644 --- a/feature/camera/build.gradle.kts +++ b/feature/uploadphoto/build.gradle.kts @@ -5,7 +5,7 @@ plugins { } android { - namespace = "com.nexters.ilab.android.feature.camera" + namespace = "com.nexters.ilab.android.feature.uploadphoto" } dependencies { diff --git a/feature/camera/src/main/kotlin/com/nexters/ilab/android/feature/camera/UploadCheckScreen.kt b/feature/uploadphoto/src/main/kotlin/com/nexters/ilab/android/feature/uploadphoto/UploadCheckScreen.kt similarity index 99% rename from feature/camera/src/main/kotlin/com/nexters/ilab/android/feature/camera/UploadCheckScreen.kt rename to feature/uploadphoto/src/main/kotlin/com/nexters/ilab/android/feature/uploadphoto/UploadCheckScreen.kt index 5a557dcf..53d8f5e6 100644 --- a/feature/camera/src/main/kotlin/com/nexters/ilab/android/feature/camera/UploadCheckScreen.kt +++ b/feature/uploadphoto/src/main/kotlin/com/nexters/ilab/android/feature/uploadphoto/UploadCheckScreen.kt @@ -1,4 +1,4 @@ -package com.nexters.ilab.android.feature.camera +package com.nexters.ilab.android.feature.uploadphoto import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.PickVisualMediaRequest diff --git a/feature/camera/src/main/kotlin/com/nexters/ilab/android/feature/camera/UploadPhotoScreen.kt b/feature/uploadphoto/src/main/kotlin/com/nexters/ilab/android/feature/uploadphoto/UploadPhotoScreen.kt similarity index 99% rename from feature/camera/src/main/kotlin/com/nexters/ilab/android/feature/camera/UploadPhotoScreen.kt rename to feature/uploadphoto/src/main/kotlin/com/nexters/ilab/android/feature/uploadphoto/UploadPhotoScreen.kt index 92188f6f..e61caf34 100644 --- a/feature/camera/src/main/kotlin/com/nexters/ilab/android/feature/camera/UploadPhotoScreen.kt +++ b/feature/uploadphoto/src/main/kotlin/com/nexters/ilab/android/feature/uploadphoto/UploadPhotoScreen.kt @@ -1,4 +1,4 @@ -package com.nexters.ilab.android.feature.camera +package com.nexters.ilab.android.feature.uploadphoto import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.PickVisualMediaRequest diff --git a/feature/camera/src/main/kotlin/com/nexters/ilab/android/feature/camera/UploadPhotoSideEffect.kt b/feature/uploadphoto/src/main/kotlin/com/nexters/ilab/android/feature/uploadphoto/UploadPhotoSideEffect.kt similarity index 79% rename from feature/camera/src/main/kotlin/com/nexters/ilab/android/feature/camera/UploadPhotoSideEffect.kt rename to feature/uploadphoto/src/main/kotlin/com/nexters/ilab/android/feature/uploadphoto/UploadPhotoSideEffect.kt index 75d52766..11cb240c 100644 --- a/feature/camera/src/main/kotlin/com/nexters/ilab/android/feature/camera/UploadPhotoSideEffect.kt +++ b/feature/uploadphoto/src/main/kotlin/com/nexters/ilab/android/feature/uploadphoto/UploadPhotoSideEffect.kt @@ -1,4 +1,4 @@ -package com.nexters.ilab.android.feature.camera +package com.nexters.ilab.android.feature.uploadphoto sealed interface UploadPhotoSideEffect { data object openPhotoPicker : UploadPhotoSideEffect diff --git a/feature/camera/src/main/kotlin/com/nexters/ilab/android/feature/camera/UploadPhotoState.kt b/feature/uploadphoto/src/main/kotlin/com/nexters/ilab/android/feature/uploadphoto/UploadPhotoState.kt similarity index 57% rename from feature/camera/src/main/kotlin/com/nexters/ilab/android/feature/camera/UploadPhotoState.kt rename to feature/uploadphoto/src/main/kotlin/com/nexters/ilab/android/feature/uploadphoto/UploadPhotoState.kt index c08ecb7c..62d51e73 100644 --- a/feature/camera/src/main/kotlin/com/nexters/ilab/android/feature/camera/UploadPhotoState.kt +++ b/feature/uploadphoto/src/main/kotlin/com/nexters/ilab/android/feature/uploadphoto/UploadPhotoState.kt @@ -1,4 +1,4 @@ -package com.nexters.ilab.android.feature.camera +package com.nexters.ilab.android.feature.uploadphoto data class UploadPhotoState( val selectedPhotoUri: String = "", diff --git a/feature/camera/src/main/kotlin/com/nexters/ilab/android/feature/camera/UploadPhotoViewModel.kt b/feature/uploadphoto/src/main/kotlin/com/nexters/ilab/android/feature/uploadphoto/UploadPhotoViewModel.kt similarity index 95% rename from feature/camera/src/main/kotlin/com/nexters/ilab/android/feature/camera/UploadPhotoViewModel.kt rename to feature/uploadphoto/src/main/kotlin/com/nexters/ilab/android/feature/uploadphoto/UploadPhotoViewModel.kt index 1f765248..8d8f6964 100644 --- a/feature/camera/src/main/kotlin/com/nexters/ilab/android/feature/camera/UploadPhotoViewModel.kt +++ b/feature/uploadphoto/src/main/kotlin/com/nexters/ilab/android/feature/uploadphoto/UploadPhotoViewModel.kt @@ -1,4 +1,4 @@ -package com.nexters.ilab.android.feature.camera +package com.nexters.ilab.android.feature.uploadphoto import androidx.lifecycle.ViewModel import dagger.hilt.android.lifecycle.HiltViewModel diff --git a/feature/camera/src/main/kotlin/com/nexters/ilab/android/feature/camera/navigation/CameraNavigation.kt b/feature/uploadphoto/src/main/kotlin/com/nexters/ilab/android/feature/uploadphoto/navigation/UploadPhotoNavigation.kt similarity index 78% rename from feature/camera/src/main/kotlin/com/nexters/ilab/android/feature/camera/navigation/CameraNavigation.kt rename to feature/uploadphoto/src/main/kotlin/com/nexters/ilab/android/feature/uploadphoto/navigation/UploadPhotoNavigation.kt index aaec6f14..f63c3997 100644 --- a/feature/camera/src/main/kotlin/com/nexters/ilab/android/feature/camera/navigation/CameraNavigation.kt +++ b/feature/uploadphoto/src/main/kotlin/com/nexters/ilab/android/feature/uploadphoto/navigation/UploadPhotoNavigation.kt @@ -1,4 +1,4 @@ -package com.nexters.ilab.android.feature.camera.navigation +package com.nexters.ilab.android.feature.uploadphoto.navigation import androidx.compose.runtime.Composable import androidx.compose.runtime.remember @@ -11,30 +11,30 @@ import androidx.navigation.NavHostController import androidx.navigation.NavOptions import androidx.navigation.compose.composable import androidx.navigation.compose.navigation -import com.nexters.ilab.android.feature.camera.UploadCheckRoute -import com.nexters.ilab.android.feature.camera.UploadPhotoRoute -import com.nexters.ilab.android.feature.camera.UploadPhotoViewModel +import com.nexters.ilab.android.feature.uploadphoto.UploadCheckRoute +import com.nexters.ilab.android.feature.uploadphoto.UploadPhotoRoute +import com.nexters.ilab.android.feature.uploadphoto.UploadPhotoViewModel -const val CAMERA_ROUTE = "camera_route" +const val UPLOAD_PHOTO_ROUTE = "upload_photo_route" const val UPLOAD_ROUTE = "upload_route" const val UPLOAD_CHECK_ROUTE = "upload_check_route" -fun NavController.navigateToCamera(navOptions: NavOptions) { - navigate(CAMERA_ROUTE, navOptions) +fun NavController.navigateToUploadPhoto(navOptions: NavOptions) { + navigate(UPLOAD_PHOTO_ROUTE, navOptions) } fun NavController.navigateToUploadCheck() { navigate(UPLOAD_CHECK_ROUTE) } -fun NavGraphBuilder.cameraNavGraph( +fun NavGraphBuilder.uploadPhotoNavGraph( navController: NavHostController, onBackClick: () -> Unit, onNavigateToUploadCheck: () -> Unit, ) { navigation( startDestination = UPLOAD_ROUTE, - route = CAMERA_ROUTE, + route = UPLOAD_PHOTO_ROUTE, ) { composable(route = UPLOAD_ROUTE) { entry -> val viewModel = entry.sharedViewModel(navController) diff --git a/settings.gradle.kts b/settings.gradle.kts index 60777f27..a4a828b9 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -31,7 +31,6 @@ include( ":core:datastore", ":core:ui", - ":feature:camera", ":feature:home", ":feature:intro", ":feature:login", @@ -39,4 +38,5 @@ include( ":feature:mypage", ":feature:navigator", ":feature:setting", + ":feature:uploadphoto", ) From 97f0b5487f63e0db79c822d3e02832101dff7ab0 Mon Sep 17 00:00:00 2001 From: JI HUN LEE <51016231+easyhooon@users.noreply.github.com> Date: Thu, 1 Feb 2024 06:11:37 +0900 Subject: [PATCH 3/7] =?UTF-8?q?[FEAT]=20=EC=B9=B4=EB=A9=94=EB=9D=BC=20?= =?UTF-8?q?=EA=B6=8C=ED=95=9C=20=EC=84=A4=EC=A0=95=20=EB=B0=8F=20=EC=B9=B4?= =?UTF-8?q?=EB=A9=94=EB=9D=BC=20=EC=8B=A4=ED=96=89=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/AndroidManifest.xml | 5 ++ .../android/core/common/extension/Activity.kt | 9 ++ .../android/core/common/extension/Context.kt | 15 ++++ .../src/main/res/values/strings.xml | 6 ++ .../feature/uploadphoto/PermissionDialog.kt | 90 +++++++++++++++++++ .../feature/uploadphoto/UploadCheckScreen.kt | 4 +- .../feature/uploadphoto/UploadPhotoScreen.kt | 63 ++++++++++--- .../uploadphoto/UploadPhotoSideEffect.kt | 3 +- .../feature/uploadphoto/UploadPhotoState.kt | 1 + .../uploadphoto/UploadPhotoViewModel.kt | 27 ++++-- .../navigation/UploadPhotoNavigation.kt | 1 - 11 files changed, 202 insertions(+), 22 deletions(-) create mode 100644 core/common/src/main/kotlin/com/nexters/ilab/android/core/common/extension/Context.kt create mode 100644 feature/uploadphoto/src/main/kotlin/com/nexters/ilab/android/feature/uploadphoto/PermissionDialog.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index bb29ccca..f1b2dc02 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,7 +2,12 @@ + + + Activity.startActivityWithAnimation( withFinish: Boolean, @@ -21,3 +23,10 @@ inline fun Activity.startActivityWithAnimation( } if (withFinish) finish() } + +fun Activity.openAppSettings() { + Intent( + Settings.ACTION_APPLICATION_DETAILS_SETTINGS, + Uri.fromParts("package", packageName, null), + ).also(::startActivity) +} diff --git a/core/common/src/main/kotlin/com/nexters/ilab/android/core/common/extension/Context.kt b/core/common/src/main/kotlin/com/nexters/ilab/android/core/common/extension/Context.kt new file mode 100644 index 00000000..0a6fccf0 --- /dev/null +++ b/core/common/src/main/kotlin/com/nexters/ilab/android/core/common/extension/Context.kt @@ -0,0 +1,15 @@ +package com.nexters.ilab.android.core.common.extension + +import android.app.Activity +import android.content.Context +import android.content.ContextWrapper + +// https://stackoverflow.com/questions/64675386/how-to-get-activity-in-compose +fun Context.findActivity(): Activity { + var context = this + while (context is ContextWrapper) { + if (context is Activity) return context + context = context.baseContext + } + error("Permissions should be called in the context of an Activity") +} diff --git a/core/designsystem/src/main/res/values/strings.xml b/core/designsystem/src/main/res/values/strings.xml index 95f1c8c1..78bd86ec 100644 --- a/core/designsystem/src/main/res/values/strings.xml +++ b/core/designsystem/src/main/res/values/strings.xml @@ -26,4 +26,10 @@ 내가 제일 잘나왔다고 생각하는 사진을 선택해주세요. 어둡거나 흐린 사진은 피해주세요. 안경이나 마스크, 모자 등 얼굴을 가린 사진은 피해주세요. + 권한 필요 + 사진을 업로드 하기 위해서는 권한이 필요합니다. + 앱 설정으로 이동 + "카메라 권한을 거부하였습니다. 앱 설정으로 이동하여 권한을 부여할 수 있습니다." + + diff --git a/feature/uploadphoto/src/main/kotlin/com/nexters/ilab/android/feature/uploadphoto/PermissionDialog.kt b/feature/uploadphoto/src/main/kotlin/com/nexters/ilab/android/feature/uploadphoto/PermissionDialog.kt new file mode 100644 index 00000000..f635484e --- /dev/null +++ b/feature/uploadphoto/src/main/kotlin/com/nexters/ilab/android/feature/uploadphoto/PermissionDialog.kt @@ -0,0 +1,90 @@ +package com.nexters.ilab.android.feature.uploadphoto + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.BasicAlertDialog +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.nexters.ilab.android.core.designsystem.R + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun PermissionDialog( + permissionTextProvider: PermissionTextProvider, + isPermanentlyDeclined: Boolean, + onDismiss: () -> Unit, + onOkClick: () -> Unit, + onGoToAppSettingsClick: () -> Unit, + modifier: Modifier = Modifier, +) { + BasicAlertDialog( + onDismissRequest = onDismiss, + content = { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + ) { + Text(text = stringResource(id = R.string.permission_required)) + Text( + text = permissionTextProvider.getDescription( + isPermanentlyDeclined = isPermanentlyDeclined, + ), + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 16.dp), + ) + HorizontalDivider() + Text( + text = if (isPermanentlyDeclined) { + stringResource(id = R.string.go_to_app_setting) + } else { + stringResource(id = R.string.check) + }, + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .clickable { + if (isPermanentlyDeclined) { + onGoToAppSettingsClick() + } else { + onOkClick() + } + } + .padding(16.dp), + ) + } + }, + modifier = modifier + .clip(RoundedCornerShape(12.dp)) + .background(color = Color.White), + ) +} + +interface PermissionTextProvider { + fun getDescription(isPermanentlyDeclined: Boolean): String +} + +class CameraPermissionTextProvider : PermissionTextProvider { + override fun getDescription(isPermanentlyDeclined: Boolean): String { + return if (isPermanentlyDeclined) { + "카메라 권한을 거부하였습니다. 앱 설정으로 이동하여 권한을 부여할 수 있습니다." + } else { + "프로필 사진을 만들기 위해서는 카메라 접근 권한이 필요합니다." + } + } +} diff --git a/feature/uploadphoto/src/main/kotlin/com/nexters/ilab/android/feature/uploadphoto/UploadCheckScreen.kt b/feature/uploadphoto/src/main/kotlin/com/nexters/ilab/android/feature/uploadphoto/UploadCheckScreen.kt index 53d8f5e6..dbdb443d 100644 --- a/feature/uploadphoto/src/main/kotlin/com/nexters/ilab/android/feature/uploadphoto/UploadCheckScreen.kt +++ b/feature/uploadphoto/src/main/kotlin/com/nexters/ilab/android/feature/uploadphoto/UploadCheckScreen.kt @@ -65,8 +65,8 @@ internal fun UploadCheckRoute( PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly), ) } - - is UploadPhotoSideEffect.openCamera -> {} + is UploadPhotoSideEffect.requestCameraPermission -> {} + is UploadPhotoSideEffect.startCamera -> {} is UploadPhotoSideEffect.UploadPhotoSuccess -> {} } } diff --git a/feature/uploadphoto/src/main/kotlin/com/nexters/ilab/android/feature/uploadphoto/UploadPhotoScreen.kt b/feature/uploadphoto/src/main/kotlin/com/nexters/ilab/android/feature/uploadphoto/UploadPhotoScreen.kt index e61caf34..e0b7f390 100644 --- a/feature/uploadphoto/src/main/kotlin/com/nexters/ilab/android/feature/uploadphoto/UploadPhotoScreen.kt +++ b/feature/uploadphoto/src/main/kotlin/com/nexters/ilab/android/feature/uploadphoto/UploadPhotoScreen.kt @@ -1,5 +1,8 @@ package com.nexters.ilab.android.feature.uploadphoto +import android.Manifest +import android.content.Intent +import android.provider.MediaStore import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.PickVisualMediaRequest import androidx.activity.result.contract.ActivityResultContracts @@ -23,6 +26,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString @@ -30,6 +34,8 @@ import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.nexters.ilab.android.core.common.extension.findActivity +import com.nexters.ilab.android.core.common.extension.openAppSettings import com.nexters.ilab.android.core.designsystem.R import com.nexters.ilab.android.core.designsystem.theme.Contents2 import com.nexters.ilab.android.core.designsystem.theme.PurpleBlue200 @@ -46,7 +52,6 @@ import com.nexters.ilab.core.ui.component.TopAppBarNavigationType import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf -@Suppress("unused") @Composable internal fun UploadPhotoRoute( onBackClick: () -> Unit, @@ -54,10 +59,18 @@ internal fun UploadPhotoRoute( viewModel: UploadPhotoViewModel = hiltViewModel(), ) { val uiState by viewModel.container.stateFlow.collectAsStateWithLifecycle() + val activity = LocalContext.current.findActivity() val singlePhotoPickerLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.PickVisualMedia(), - onResult = { uri -> viewModel.setSelectImageUri(uri.toString()) } + onResult = { uri -> viewModel.setSelectImageUri(uri.toString()) }, + ) + + val cameraPermissionResultLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.RequestPermission(), + onResult = { isGranted -> + viewModel.onPermissionResult(isGranted = isGranted) + }, ) LaunchedEffect(viewModel) { @@ -68,7 +81,17 @@ internal fun UploadPhotoRoute( PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly), ) } - is UploadPhotoSideEffect.openCamera -> {} + + is UploadPhotoSideEffect.requestCameraPermission -> { + cameraPermissionResultLauncher.launch(Manifest.permission.CAMERA) + } + + is UploadPhotoSideEffect.startCamera -> { + // TODO 콜백을 통해 이미지를 받아와야 함 + val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE) + activity.startActivity(intent) + } + is UploadPhotoSideEffect.UploadPhotoSuccess -> onNavigateToUploadCheck() } } @@ -77,8 +100,9 @@ internal fun UploadPhotoRoute( UploadPhotoScreen( uiState = uiState, onBackClick = onBackClick, - onPhotoPickerClick = viewModel::onPhotoPickerClick, - onCameraClick = viewModel::onCameraClick, + openPhotoPicker = viewModel::openPhotoPicker, + requestCameraPermission = viewModel::requestCameraPermission, + dismissPermissionDialog = viewModel::dismissPermissionDialog, ) } @@ -86,18 +110,34 @@ internal fun UploadPhotoRoute( internal fun UploadPhotoScreen( uiState: UploadPhotoState, onBackClick: () -> Unit, - onPhotoPickerClick: () -> Unit, - onCameraClick: () -> Unit, + openPhotoPicker: () -> Unit, + requestCameraPermission: () -> Unit, + dismissPermissionDialog: () -> Unit, ) { + val activity = LocalContext.current.findActivity() + Column( modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally, ) { + if (uiState.isPermissionDialogVisible) { + PermissionDialog( + permissionTextProvider = CameraPermissionTextProvider(), + isPermanentlyDeclined = !activity.shouldShowRequestPermissionRationale(Manifest.permission.CAMERA), + onDismiss = dismissPermissionDialog, + onOkClick = { + dismissPermissionDialog() + requestCameraPermission() + }, + onGoToAppSettingsClick = { activity.openAppSettings() }, + ) + } + UploadPhotoTopAppBar(onBackClick = onBackClick) UploadPhotoContent( - onPhotoPickerClick = onPhotoPickerClick, - onCameraClick = onCameraClick, + onPhotoPickerClick = openPhotoPicker, + onCameraClick = requestCameraPermission, ) } } @@ -240,7 +280,8 @@ fun UploadPhotoScreenPreview() { UploadPhotoScreen( uiState = UploadPhotoState(), onBackClick = {}, - onPhotoPickerClick = {}, - onCameraClick = {}, + openPhotoPicker = {}, + requestCameraPermission = {}, + dismissPermissionDialog = {}, ) } diff --git a/feature/uploadphoto/src/main/kotlin/com/nexters/ilab/android/feature/uploadphoto/UploadPhotoSideEffect.kt b/feature/uploadphoto/src/main/kotlin/com/nexters/ilab/android/feature/uploadphoto/UploadPhotoSideEffect.kt index 11cb240c..71410ec2 100644 --- a/feature/uploadphoto/src/main/kotlin/com/nexters/ilab/android/feature/uploadphoto/UploadPhotoSideEffect.kt +++ b/feature/uploadphoto/src/main/kotlin/com/nexters/ilab/android/feature/uploadphoto/UploadPhotoSideEffect.kt @@ -2,6 +2,7 @@ package com.nexters.ilab.android.feature.uploadphoto sealed interface UploadPhotoSideEffect { data object openPhotoPicker : UploadPhotoSideEffect - data object openCamera : UploadPhotoSideEffect + data object requestCameraPermission : UploadPhotoSideEffect + data object startCamera : UploadPhotoSideEffect data object UploadPhotoSuccess : UploadPhotoSideEffect } diff --git a/feature/uploadphoto/src/main/kotlin/com/nexters/ilab/android/feature/uploadphoto/UploadPhotoState.kt b/feature/uploadphoto/src/main/kotlin/com/nexters/ilab/android/feature/uploadphoto/UploadPhotoState.kt index 62d51e73..372ae38c 100644 --- a/feature/uploadphoto/src/main/kotlin/com/nexters/ilab/android/feature/uploadphoto/UploadPhotoState.kt +++ b/feature/uploadphoto/src/main/kotlin/com/nexters/ilab/android/feature/uploadphoto/UploadPhotoState.kt @@ -2,4 +2,5 @@ package com.nexters.ilab.android.feature.uploadphoto data class UploadPhotoState( val selectedPhotoUri: String = "", + val isPermissionDialogVisible: Boolean = false, ) diff --git a/feature/uploadphoto/src/main/kotlin/com/nexters/ilab/android/feature/uploadphoto/UploadPhotoViewModel.kt b/feature/uploadphoto/src/main/kotlin/com/nexters/ilab/android/feature/uploadphoto/UploadPhotoViewModel.kt index 8d8f6964..53404303 100644 --- a/feature/uploadphoto/src/main/kotlin/com/nexters/ilab/android/feature/uploadphoto/UploadPhotoViewModel.kt +++ b/feature/uploadphoto/src/main/kotlin/com/nexters/ilab/android/feature/uploadphoto/UploadPhotoViewModel.kt @@ -14,21 +14,34 @@ class UploadPhotoViewModel @Inject constructor() : ViewModel(), ContainerHost(UploadPhotoState()) - fun onPhotoPickerClick() = intent { + fun openPhotoPicker() = intent { postSideEffect(UploadPhotoSideEffect.openPhotoPicker) } - fun onCameraClick() = intent { - postSideEffect(UploadPhotoSideEffect.openCamera) + fun requestCameraPermission() = intent { + postSideEffect(UploadPhotoSideEffect.requestCameraPermission) } fun setSelectImageUri(uri: String) = intent { reduce { - state.copy( - selectedPhotoUri = uri, - ) + state.copy(selectedPhotoUri = uri) } postSideEffect(UploadPhotoSideEffect.UploadPhotoSuccess) } -} + fun dismissPermissionDialog() = intent { + reduce { + state.copy(isPermissionDialogVisible = false) + } + } + + fun onPermissionResult(isGranted: Boolean) = intent { + if (isGranted) { + postSideEffect(UploadPhotoSideEffect.startCamera) + } else { + reduce { + state.copy(isPermissionDialogVisible = true) + } + } + } +} diff --git a/feature/uploadphoto/src/main/kotlin/com/nexters/ilab/android/feature/uploadphoto/navigation/UploadPhotoNavigation.kt b/feature/uploadphoto/src/main/kotlin/com/nexters/ilab/android/feature/uploadphoto/navigation/UploadPhotoNavigation.kt index f63c3997..c24c8d74 100644 --- a/feature/uploadphoto/src/main/kotlin/com/nexters/ilab/android/feature/uploadphoto/navigation/UploadPhotoNavigation.kt +++ b/feature/uploadphoto/src/main/kotlin/com/nexters/ilab/android/feature/uploadphoto/navigation/UploadPhotoNavigation.kt @@ -66,4 +66,3 @@ inline fun NavBackStackEntry.sharedViewModel( } return hiltViewModel(parentEntry) } - From 87a1c7bfade7f23dacd18809bf05dc416ede3278 Mon Sep 17 00:00:00 2001 From: JI HUN LEE <51016231+easyhooon@users.noreply.github.com> Date: Thu, 1 Feb 2024 06:23:42 +0900 Subject: [PATCH 4/7] =?UTF-8?q?[FEAT]=20PhotoPicker=20=EC=8B=A4=ED=96=89?= =?UTF-8?q?=EC=8B=9C=20=EC=98=88=EC=99=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 사진을 고르지 않을 경우(uri 가 null 일 경우) 다음 화면으로 넘어가면 안됨 --- core/designsystem/src/main/res/values/strings.xml | 1 + .../ilab/android/feature/uploadphoto/UploadPhotoScreen.kt | 7 +++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/core/designsystem/src/main/res/values/strings.xml b/core/designsystem/src/main/res/values/strings.xml index 78bd86ec..cb00a377 100644 --- a/core/designsystem/src/main/res/values/strings.xml +++ b/core/designsystem/src/main/res/values/strings.xml @@ -30,6 +30,7 @@ 사진을 업로드 하기 위해서는 권한이 필요합니다. 앱 설정으로 이동 "카메라 권한을 거부하였습니다. 앱 설정으로 이동하여 권한을 부여할 수 있습니다." + 사진을 선택해 주세요. diff --git a/feature/uploadphoto/src/main/kotlin/com/nexters/ilab/android/feature/uploadphoto/UploadPhotoScreen.kt b/feature/uploadphoto/src/main/kotlin/com/nexters/ilab/android/feature/uploadphoto/UploadPhotoScreen.kt index e0b7f390..7564e522 100644 --- a/feature/uploadphoto/src/main/kotlin/com/nexters/ilab/android/feature/uploadphoto/UploadPhotoScreen.kt +++ b/feature/uploadphoto/src/main/kotlin/com/nexters/ilab/android/feature/uploadphoto/UploadPhotoScreen.kt @@ -59,11 +59,14 @@ internal fun UploadPhotoRoute( viewModel: UploadPhotoViewModel = hiltViewModel(), ) { val uiState by viewModel.container.stateFlow.collectAsStateWithLifecycle() - val activity = LocalContext.current.findActivity() + val context = LocalContext.current + val activity = context.findActivity() val singlePhotoPickerLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.PickVisualMedia(), - onResult = { uri -> viewModel.setSelectImageUri(uri.toString()) }, + onResult = { uri -> + uri?.let { viewModel.setSelectImageUri(it.toString()) } + }, ) val cameraPermissionResultLauncher = rememberLauncherForActivityResult( From ad1fad468b9de0a6ff386ac892961300acd374fc Mon Sep 17 00:00:00 2001 From: JI HUN LEE <51016231+easyhooon@users.noreply.github.com> Date: Thu, 1 Feb 2024 07:12:14 +0900 Subject: [PATCH 5/7] =?UTF-8?q?[FEAT]=20=EA=B0=80=EC=9D=B4=EB=93=9C=20?= =?UTF-8?q?=ED=99=95=EC=9D=B8=20=ED=99=94=EB=A9=B4=20=EC=82=AC=EC=A7=84=20?= =?UTF-8?q?=EB=B0=94=EA=BE=B8=EA=B8=B0=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 다이얼로그 호출 -> 사진 보관함 클릭 -> PhotoPicker 실행 다이얼로그 호출 -> 사진 찍기 클릭 -> 카메라 실행 --- .../feature/uploadphoto/UploadCheckScreen.kt | 54 ++++++++++++- .../feature/uploadphoto/UploadPhotoDialog.kt | 81 +++++++++++++++++++ .../feature/uploadphoto/UploadPhotoScreen.kt | 3 +- .../feature/uploadphoto/UploadPhotoState.kt | 1 + .../uploadphoto/UploadPhotoViewModel.kt | 6 ++ 5 files changed, 139 insertions(+), 6 deletions(-) create mode 100644 feature/uploadphoto/src/main/kotlin/com/nexters/ilab/android/feature/uploadphoto/UploadPhotoDialog.kt diff --git a/feature/uploadphoto/src/main/kotlin/com/nexters/ilab/android/feature/uploadphoto/UploadCheckScreen.kt b/feature/uploadphoto/src/main/kotlin/com/nexters/ilab/android/feature/uploadphoto/UploadCheckScreen.kt index dbdb443d..500bc96a 100644 --- a/feature/uploadphoto/src/main/kotlin/com/nexters/ilab/android/feature/uploadphoto/UploadCheckScreen.kt +++ b/feature/uploadphoto/src/main/kotlin/com/nexters/ilab/android/feature/uploadphoto/UploadCheckScreen.kt @@ -1,5 +1,8 @@ package com.nexters.ilab.android.feature.uploadphoto +import android.Manifest +import android.content.Intent +import android.provider.MediaStore import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.PickVisualMediaRequest import androidx.activity.result.contract.ActivityResultContracts @@ -25,11 +28,13 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.nexters.ilab.android.core.common.extension.findActivity import com.nexters.ilab.android.core.designsystem.R import com.nexters.ilab.android.core.designsystem.theme.Contents1 import com.nexters.ilab.android.core.designsystem.theme.Contents2 @@ -51,12 +56,20 @@ internal fun UploadCheckRoute( viewModel: UploadPhotoViewModel = hiltViewModel(), ) { val uiState by viewModel.container.stateFlow.collectAsStateWithLifecycle() + val activity = LocalContext.current.findActivity() val singlePhotoPickerLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.PickVisualMedia(), onResult = { uri -> viewModel.setSelectImageUri(uri.toString()) }, ) + val cameraPermissionResultLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.RequestPermission(), + onResult = { isGranted -> + viewModel.onPermissionResult(isGranted = isGranted) + }, + ) + LaunchedEffect(viewModel) { viewModel.container.sideEffectFlow.collect { sideEffect -> when (sideEffect) { @@ -65,15 +78,28 @@ internal fun UploadCheckRoute( PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly), ) } - is UploadPhotoSideEffect.requestCameraPermission -> {} - is UploadPhotoSideEffect.startCamera -> {} + + is UploadPhotoSideEffect.requestCameraPermission -> { + cameraPermissionResultLauncher.launch(Manifest.permission.CAMERA) + } + + is UploadPhotoSideEffect.startCamera -> { + // TODO 콜백을 통해 이미지를 받아와야 함 + val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE) + activity.startActivity(intent) + } + is UploadPhotoSideEffect.UploadPhotoSuccess -> {} } } } + UploadCheckScreen( uiState = uiState, onBackClick = onBackClick, + toggleUploadPhotoDialog = viewModel::toggleUploadPhotoDialog, + openPhotoPicker = viewModel::openPhotoPicker, + requestCameraPermission = viewModel::requestCameraPermission, ) } @@ -81,10 +107,24 @@ internal fun UploadCheckRoute( private fun UploadCheckScreen( uiState: UploadPhotoState, onBackClick: () -> Unit, + toggleUploadPhotoDialog: (Boolean) -> Unit, + openPhotoPicker: () -> Unit, + requestCameraPermission: () -> Unit, ) { + if (uiState.isUploadPhotoDialogVisible) { + UploadPhotoDialog( + onDismiss = { toggleUploadPhotoDialog(false) }, + openPhotoPicker = openPhotoPicker, + requestCameraPermission = requestCameraPermission, + ) + } + Column { UploadCheckTopAppBar(onBackClick = onBackClick) - UploadCheckContent(uiState.selectedPhotoUri) + UploadCheckContent( + selectedPhotoUri = uiState.selectedPhotoUri, + toggleUploadPhotoDialog = toggleUploadPhotoDialog, + ) } } @@ -106,6 +146,7 @@ private fun UploadCheckTopAppBar( @Composable private fun UploadCheckContent( selectedPhotoUri: String, + toggleUploadPhotoDialog: (Boolean) -> Unit, ) { Column( modifier = Modifier @@ -166,7 +207,9 @@ private fun UploadCheckContent( .padding(start = 4.dp, end = 4.dp, bottom = 18.dp), ) { ILabButton( - onClick = {}, + onClick = { + toggleUploadPhotoDialog(true) + }, modifier = Modifier .weight(1f) .height(60.dp) @@ -229,5 +272,8 @@ fun UploadCheckScreenPreview() { selectedPhotoUri = "", ), onBackClick = {}, + toggleUploadPhotoDialog = {}, + openPhotoPicker = {}, + requestCameraPermission = {}, ) } diff --git a/feature/uploadphoto/src/main/kotlin/com/nexters/ilab/android/feature/uploadphoto/UploadPhotoDialog.kt b/feature/uploadphoto/src/main/kotlin/com/nexters/ilab/android/feature/uploadphoto/UploadPhotoDialog.kt new file mode 100644 index 00000000..e97b756d --- /dev/null +++ b/feature/uploadphoto/src/main/kotlin/com/nexters/ilab/android/feature/uploadphoto/UploadPhotoDialog.kt @@ -0,0 +1,81 @@ +package com.nexters.ilab.android.feature.uploadphoto + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.BasicAlertDialog +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.nexters.ilab.android.core.designsystem.R +import com.nexters.ilab.android.core.designsystem.theme.Contents1 + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun UploadPhotoDialog( + onDismiss: () -> Unit, + openPhotoPicker: () -> Unit, + requestCameraPermission: () -> Unit, + modifier: Modifier = Modifier, +) { + BasicAlertDialog( + onDismissRequest = onDismiss, + content = { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable( + onClick = { + onDismiss() + openPhotoPicker() + }, + ), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = stringResource(id = R.string.photo_library), + modifier = Modifier.padding(vertical = 12.dp), + style = Contents1, + color = Color.Black, + ) + } + Row( + modifier = Modifier + .fillMaxWidth() + .clickable( + onClick = { + onDismiss() + requestCameraPermission() + }, + ), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = stringResource(id = R.string.take_photo), + modifier = Modifier.padding(vertical = 12.dp), + style = Contents1, + color = Color.Black, + ) + } + } + }, + modifier = modifier + .clip(RoundedCornerShape(12.dp)) + .background(color = Color.White), + ) +} diff --git a/feature/uploadphoto/src/main/kotlin/com/nexters/ilab/android/feature/uploadphoto/UploadPhotoScreen.kt b/feature/uploadphoto/src/main/kotlin/com/nexters/ilab/android/feature/uploadphoto/UploadPhotoScreen.kt index 7564e522..e94053a3 100644 --- a/feature/uploadphoto/src/main/kotlin/com/nexters/ilab/android/feature/uploadphoto/UploadPhotoScreen.kt +++ b/feature/uploadphoto/src/main/kotlin/com/nexters/ilab/android/feature/uploadphoto/UploadPhotoScreen.kt @@ -59,8 +59,7 @@ internal fun UploadPhotoRoute( viewModel: UploadPhotoViewModel = hiltViewModel(), ) { val uiState by viewModel.container.stateFlow.collectAsStateWithLifecycle() - val context = LocalContext.current - val activity = context.findActivity() + val activity = LocalContext.current.findActivity() val singlePhotoPickerLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.PickVisualMedia(), diff --git a/feature/uploadphoto/src/main/kotlin/com/nexters/ilab/android/feature/uploadphoto/UploadPhotoState.kt b/feature/uploadphoto/src/main/kotlin/com/nexters/ilab/android/feature/uploadphoto/UploadPhotoState.kt index 372ae38c..f6b57d7d 100644 --- a/feature/uploadphoto/src/main/kotlin/com/nexters/ilab/android/feature/uploadphoto/UploadPhotoState.kt +++ b/feature/uploadphoto/src/main/kotlin/com/nexters/ilab/android/feature/uploadphoto/UploadPhotoState.kt @@ -3,4 +3,5 @@ package com.nexters.ilab.android.feature.uploadphoto data class UploadPhotoState( val selectedPhotoUri: String = "", val isPermissionDialogVisible: Boolean = false, + val isUploadPhotoDialogVisible: Boolean = false, ) diff --git a/feature/uploadphoto/src/main/kotlin/com/nexters/ilab/android/feature/uploadphoto/UploadPhotoViewModel.kt b/feature/uploadphoto/src/main/kotlin/com/nexters/ilab/android/feature/uploadphoto/UploadPhotoViewModel.kt index 53404303..913d7230 100644 --- a/feature/uploadphoto/src/main/kotlin/com/nexters/ilab/android/feature/uploadphoto/UploadPhotoViewModel.kt +++ b/feature/uploadphoto/src/main/kotlin/com/nexters/ilab/android/feature/uploadphoto/UploadPhotoViewModel.kt @@ -44,4 +44,10 @@ class UploadPhotoViewModel @Inject constructor() : ViewModel(), ContainerHost Date: Thu, 1 Feb 2024 07:14:11 +0900 Subject: [PATCH 6/7] =?UTF-8?q?[FEAT]=20=EA=B0=80=EC=9D=B4=EB=93=9C=20?= =?UTF-8?q?=ED=99=95=EC=9D=B8=20=ED=99=94=EB=A9=B4=20Image=20=EC=98=B5?= =?UTF-8?q?=EC=85=98=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Crop 으로 변경 -> 정사각형으로 이미지가 꽉차게 보이도록 --- .../src/main/kotlin/com/nexters/ilab/core/ui/component/Image.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/ui/src/main/kotlin/com/nexters/ilab/core/ui/component/Image.kt b/core/ui/src/main/kotlin/com/nexters/ilab/core/ui/component/Image.kt index 7730f2e0..5be39470 100644 --- a/core/ui/src/main/kotlin/com/nexters/ilab/core/ui/component/Image.kt +++ b/core/ui/src/main/kotlin/com/nexters/ilab/core/ui/component/Image.kt @@ -68,7 +68,7 @@ fun NetworkImage( .crossfade(true) .build(), contentDescription = contentDescription, - contentScale = ContentScale.Fit, + contentScale = ContentScale.Crop, modifier = modifier, ) } From 3ade9e3e9d890236658318b5288ee477ccce9798 Mon Sep 17 00:00:00 2001 From: JI HUN LEE <51016231+easyhooon@users.noreply.github.com> Date: Thu, 1 Feb 2024 07:59:34 +0900 Subject: [PATCH 7/7] =?UTF-8?q?[FEAT]=20=EC=B9=B4=EB=A9=94=EB=9D=BC?= =?UTF-8?q?=EB=A1=9C=20=EC=82=AC=EC=A7=84=EC=9D=84=20=EC=B0=8D=EC=96=B4=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4=EC=97=90=20=EC=B6=9C=EB=A0=A5=ED=95=98?= =?UTF-8?q?=EB=8A=94=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../android/core/common/extension/Bitmap.kt | 38 +++++++++++++++++++ .../feature/uploadphoto/UploadCheckScreen.kt | 21 ++++++---- .../feature/uploadphoto/UploadPhotoScreen.kt | 16 +++++--- 3 files changed, 61 insertions(+), 14 deletions(-) create mode 100644 core/common/src/main/kotlin/com/nexters/ilab/android/core/common/extension/Bitmap.kt diff --git a/core/common/src/main/kotlin/com/nexters/ilab/android/core/common/extension/Bitmap.kt b/core/common/src/main/kotlin/com/nexters/ilab/android/core/common/extension/Bitmap.kt new file mode 100644 index 00000000..c0ec2c4b --- /dev/null +++ b/core/common/src/main/kotlin/com/nexters/ilab/android/core/common/extension/Bitmap.kt @@ -0,0 +1,38 @@ +package com.nexters.ilab.android.core.common.extension + +import android.content.ContentValues +import android.content.Context +import android.graphics.Bitmap +import android.net.Uri +import android.os.Build +import android.os.Environment +import android.provider.MediaStore +import java.io.ByteArrayOutputStream + +fun Bitmap.toUri(context: Context): Uri { + val filename = "${System.currentTimeMillis()}.png" + val stream = ByteArrayOutputStream() + this.compress(Bitmap.CompressFormat.PNG, 100, stream) + val imageCollection = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) + } else { + MediaStore.Images.Media.EXTERNAL_CONTENT_URI + } + + val contentValues = ContentValues().apply { + put(MediaStore.Images.Media.DISPLAY_NAME, filename) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES) + } + } + + val imageUri = context.contentResolver.insert(imageCollection, contentValues) + imageUri?.let { uri -> + context.contentResolver.openOutputStream(uri).use { outputStream -> + if (outputStream != null) { + this.compress(Bitmap.CompressFormat.PNG, 100, outputStream) + } + } + return uri + } ?: error("Failed to create new MediaStore record.") +} diff --git a/feature/uploadphoto/src/main/kotlin/com/nexters/ilab/android/feature/uploadphoto/UploadCheckScreen.kt b/feature/uploadphoto/src/main/kotlin/com/nexters/ilab/android/feature/uploadphoto/UploadCheckScreen.kt index 500bc96a..43d9274f 100644 --- a/feature/uploadphoto/src/main/kotlin/com/nexters/ilab/android/feature/uploadphoto/UploadCheckScreen.kt +++ b/feature/uploadphoto/src/main/kotlin/com/nexters/ilab/android/feature/uploadphoto/UploadCheckScreen.kt @@ -1,8 +1,6 @@ package com.nexters.ilab.android.feature.uploadphoto import android.Manifest -import android.content.Intent -import android.provider.MediaStore import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.PickVisualMediaRequest import androidx.activity.result.contract.ActivityResultContracts @@ -34,7 +32,7 @@ import androidx.compose.ui.res.vectorResource import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.nexters.ilab.android.core.common.extension.findActivity +import com.nexters.ilab.android.core.common.extension.toUri import com.nexters.ilab.android.core.designsystem.R import com.nexters.ilab.android.core.designsystem.theme.Contents1 import com.nexters.ilab.android.core.designsystem.theme.Contents2 @@ -56,11 +54,13 @@ internal fun UploadCheckRoute( viewModel: UploadPhotoViewModel = hiltViewModel(), ) { val uiState by viewModel.container.stateFlow.collectAsStateWithLifecycle() - val activity = LocalContext.current.findActivity() + val context = LocalContext.current val singlePhotoPickerLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.PickVisualMedia(), - onResult = { uri -> viewModel.setSelectImageUri(uri.toString()) }, + onResult = { uri -> + uri?.let { viewModel.setSelectImageUri(it.toString()) } + }, ) val cameraPermissionResultLauncher = rememberLauncherForActivityResult( @@ -70,6 +70,13 @@ internal fun UploadCheckRoute( }, ) + val cameraLauncher = rememberLauncherForActivityResult(ActivityResultContracts.TakePicturePreview()) { bitmap -> + bitmap?.let { + val photoUri = it.toUri(context) + viewModel.setSelectImageUri(photoUri.toString()) + } + } + LaunchedEffect(viewModel) { viewModel.container.sideEffectFlow.collect { sideEffect -> when (sideEffect) { @@ -84,9 +91,7 @@ internal fun UploadCheckRoute( } is UploadPhotoSideEffect.startCamera -> { - // TODO 콜백을 통해 이미지를 받아와야 함 - val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE) - activity.startActivity(intent) + cameraLauncher.launch(null) } is UploadPhotoSideEffect.UploadPhotoSuccess -> {} diff --git a/feature/uploadphoto/src/main/kotlin/com/nexters/ilab/android/feature/uploadphoto/UploadPhotoScreen.kt b/feature/uploadphoto/src/main/kotlin/com/nexters/ilab/android/feature/uploadphoto/UploadPhotoScreen.kt index e94053a3..249bea78 100644 --- a/feature/uploadphoto/src/main/kotlin/com/nexters/ilab/android/feature/uploadphoto/UploadPhotoScreen.kt +++ b/feature/uploadphoto/src/main/kotlin/com/nexters/ilab/android/feature/uploadphoto/UploadPhotoScreen.kt @@ -1,8 +1,6 @@ package com.nexters.ilab.android.feature.uploadphoto import android.Manifest -import android.content.Intent -import android.provider.MediaStore import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.PickVisualMediaRequest import androidx.activity.result.contract.ActivityResultContracts @@ -36,6 +34,7 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.nexters.ilab.android.core.common.extension.findActivity import com.nexters.ilab.android.core.common.extension.openAppSettings +import com.nexters.ilab.android.core.common.extension.toUri import com.nexters.ilab.android.core.designsystem.R import com.nexters.ilab.android.core.designsystem.theme.Contents2 import com.nexters.ilab.android.core.designsystem.theme.PurpleBlue200 @@ -59,7 +58,7 @@ internal fun UploadPhotoRoute( viewModel: UploadPhotoViewModel = hiltViewModel(), ) { val uiState by viewModel.container.stateFlow.collectAsStateWithLifecycle() - val activity = LocalContext.current.findActivity() + val context = LocalContext.current val singlePhotoPickerLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.PickVisualMedia(), @@ -75,6 +74,13 @@ internal fun UploadPhotoRoute( }, ) + val cameraLauncher = rememberLauncherForActivityResult(ActivityResultContracts.TakePicturePreview()) { bitmap -> + bitmap?.let { + val photoUri = it.toUri(context) + viewModel.setSelectImageUri(photoUri.toString()) + } + } + LaunchedEffect(viewModel) { viewModel.container.sideEffectFlow.collect { sideEffect -> when (sideEffect) { @@ -89,9 +95,7 @@ internal fun UploadPhotoRoute( } is UploadPhotoSideEffect.startCamera -> { - // TODO 콜백을 통해 이미지를 받아와야 함 - val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE) - activity.startActivity(intent) + cameraLauncher.launch(null) } is UploadPhotoSideEffect.UploadPhotoSuccess -> onNavigateToUploadCheck()