diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 218e671..09b17cc 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -4,6 +4,13 @@
+
+
+
+
+
=
- appPreferencesFlow.map {
- it.internalCameraAspectRatio
- }
-
- override suspend fun setInterenalCameraAspectRatio(
- @IntRange(from = -1, to = 1) aspectRatio: Int
- ) {
- appPreferences.updateData {
- it.toBuilder()
- .setInternalCameraAspectRatio(aspectRatio)
- .build()
- }
- }
override suspend fun clearAll() {
appPreferences.updateData {
diff --git a/domain/src/main/java/com/foke/together/domain/interactor/CaptureWithInternalCameraUseCase.kt b/domain/src/main/java/com/foke/together/domain/interactor/CaptureWithInternalCameraUseCase.kt
deleted file mode 100644
index 4f6d691..0000000
--- a/domain/src/main/java/com/foke/together/domain/interactor/CaptureWithInternalCameraUseCase.kt
+++ /dev/null
@@ -1,34 +0,0 @@
-package com.foke.together.domain.interactor
-
-import android.content.Context
-import com.foke.together.domain.output.ImageRepositoryInterface
-import com.foke.together.domain.output.InternalCameraRepositoryInterface
-import com.foke.together.util.AppLog
-import dagger.hilt.android.qualifiers.ApplicationContext
-import javax.inject.Inject
-
-class CaptureWithInternalCameraUseCase @Inject constructor(
- private val internalCameraRepository: InternalCameraRepositoryInterface,
- private val imageRepository: ImageRepositoryInterface
-) {
- suspend operator fun invoke(
- context: Context,
- fileName: String
- ): Result{
- internalCameraRepository.capture(context)
- .onSuccess {
- AppLog.i(TAG, "invoke", "success: $it")
- imageRepository.cachingImage(it, fileName)
- return Result.success(Unit)
- }
- .onFailure {
- AppLog.e(TAG, "invoke", "failure: $it")
- return Result.failure(it)
- }
- return Result.failure(Exception("Unknown error"))
- }
-
- companion object {
- private val TAG = CaptureWithInternalCameraUseCase::class.java.simpleName
- }
-}
\ No newline at end of file
diff --git a/domain/src/main/java/com/foke/together/domain/interactor/GeneratePhotoFrameUseCaseV1.kt b/domain/src/main/java/com/foke/together/domain/interactor/GeneratePhotoFrameUseCaseV1.kt
index 6b3a1c4..4b7683b 100644
--- a/domain/src/main/java/com/foke/together/domain/interactor/GeneratePhotoFrameUseCaseV1.kt
+++ b/domain/src/main/java/com/foke/together/domain/interactor/GeneratePhotoFrameUseCaseV1.kt
@@ -3,6 +3,7 @@ package com.foke.together.domain.interactor
import android.content.Context
import android.graphics.Bitmap
import android.net.Uri
+import com.foke.together.domain.interactor.entity.CameraSourceType
import com.foke.together.domain.output.ImageRepositoryInterface
import com.foke.together.util.AppPolicy
import dagger.hilt.android.qualifiers.ApplicationContext
@@ -16,7 +17,7 @@ class GeneratePhotoFrameUseCaseV1 @Inject constructor(
// 촬영한 이미지 리스트 관리
// TODO: 추후 세션 관리와 엮어서 처리하기
@Deprecated("Not in use")
- fun getCapturedImageListUri(): List = imageRepositoryInterface.getCachedImageUriList()
+ fun getCapturedImageListUri(sourceType: CameraSourceType): List = imageRepositoryInterface.getCachedImageUriList(sourceType)
@Deprecated("Not in use")
suspend fun clearCapturedImageList() = imageRepositoryInterface.clearCacheDir()
diff --git a/domain/src/main/java/com/foke/together/domain/interactor/GetInternalCameraAspectRatioUseCase.kt b/domain/src/main/java/com/foke/together/domain/interactor/GetInternalCameraAspectRatioUseCase.kt
deleted file mode 100644
index 24b02f1..0000000
--- a/domain/src/main/java/com/foke/together/domain/interactor/GetInternalCameraAspectRatioUseCase.kt
+++ /dev/null
@@ -1,11 +0,0 @@
-package com.foke.together.domain.interactor
-
-import com.foke.together.domain.output.AppPreferenceInterface
-import kotlinx.coroutines.flow.Flow
-import javax.inject.Inject
-
-class GetInternalCameraAspectRatioUseCase @Inject constructor(
- private val appPreferenceRepository: AppPreferenceInterface
-) {
- operator fun invoke() : Flow = appPreferenceRepository.getInternalCameraAspectRatio()
-}
\ No newline at end of file
diff --git a/domain/src/main/java/com/foke/together/domain/interactor/GetInternalCameraPreviewUseCase.kt b/domain/src/main/java/com/foke/together/domain/interactor/InternalCameraUseCase.kt
similarity index 62%
rename from domain/src/main/java/com/foke/together/domain/interactor/GetInternalCameraPreviewUseCase.kt
rename to domain/src/main/java/com/foke/together/domain/interactor/InternalCameraUseCase.kt
index be40908..c705c4c 100644
--- a/domain/src/main/java/com/foke/together/domain/interactor/GetInternalCameraPreviewUseCase.kt
+++ b/domain/src/main/java/com/foke/together/domain/interactor/InternalCameraUseCase.kt
@@ -9,26 +9,35 @@ import androidx.lifecycle.LifecycleOwner
import com.foke.together.domain.output.InternalCameraRepositoryInterface
import javax.inject.Inject
-class GetInternalCameraPreviewUseCase @Inject constructor(
+class InternalCameraUseCase @Inject constructor(
private val internalCameraRepository: InternalCameraRepositoryInterface,
) {
- suspend operator fun invoke(
+ suspend fun initial(
context: Context,
- previewView: PreviewView,
lifecycleOwner: LifecycleOwner,
+ previewView : PreviewView,
cameraSelector: CameraSelector,
- imageAnalysis: ImageAnalysis?,
+ imageAnalyzer: ImageAnalysis.Analyzer?,
@IntRange(from = 0, to = 2) captureMode: Int,
@IntRange(from = 0, to = 3) flashMode: Int,
- @IntRange(from = -1, to = 1) aspectRatio: Int
- ) = internalCameraRepository.showCameraPreview(
+ ) = internalCameraRepository.initial(
context = context,
lifecycleOwner = lifecycleOwner,
- previewView = previewView,
- selector = cameraSelector,
- imageAnalysis = imageAnalysis,
captureMode = captureMode,
flashMode = flashMode,
- aspectRatio = aspectRatio
+ selector = cameraSelector,
+ imageAnalyzer = imageAnalyzer,
+ previewView = previewView,
+ )
+
+ suspend fun capture(
+ context: Context,
+ fileName : String,
+ ) = internalCameraRepository.capture(
+ context = context,
+ fileName = fileName
)
+ suspend fun release(
+ context: Context
+ ) = internalCameraRepository.release(context)
}
\ No newline at end of file
diff --git a/domain/src/main/java/com/foke/together/domain/output/AppPreferenceInterface.kt b/domain/src/main/java/com/foke/together/domain/output/AppPreferenceInterface.kt
index 3b4f21e..5e600ab 100644
--- a/domain/src/main/java/com/foke/together/domain/output/AppPreferenceInterface.kt
+++ b/domain/src/main/java/com/foke/together/domain/output/AppPreferenceInterface.kt
@@ -24,14 +24,9 @@ interface AppPreferenceInterface {
)
fun getInternalCameraCaptureMode(): Flow
- suspend fun setInterenalCameraCaptureMode(
+ suspend fun setInternalCameraCaptureMode(
@IntRange(from = 0, to = 2) captureMode: Int
)
- fun getInternalCameraAspectRatio(): Flow
- suspend fun setInterenalCameraAspectRatio(
- @IntRange(from = -1, to = 1) aspectRatio: Int
- )
-
suspend fun clearAll()
}
\ No newline at end of file
diff --git a/domain/src/main/java/com/foke/together/domain/output/ImageRepositoryInterface.kt b/domain/src/main/java/com/foke/together/domain/output/ImageRepositoryInterface.kt
index cd037c1..78be19f 100644
--- a/domain/src/main/java/com/foke/together/domain/output/ImageRepositoryInterface.kt
+++ b/domain/src/main/java/com/foke/together/domain/output/ImageRepositoryInterface.kt
@@ -2,6 +2,7 @@ package com.foke.together.domain.output
import android.graphics.Bitmap
import android.net.Uri
+import com.foke.together.domain.interactor.entity.CameraSourceType
import com.foke.together.domain.interactor.entity.CutFrameTypeV1
interface ImageRepositoryInterface {
@@ -9,7 +10,8 @@ interface ImageRepositoryInterface {
suspend fun setCutFrameType(type: Int)
// 촬영한 사진들 모음
suspend fun cachingImage(image: Bitmap, fileName: String) : Uri
- fun getCachedImageUriList() : List
+
+ fun getCachedImageUriList(sourceType: CameraSourceType) : List
suspend fun clearCacheDir()
// 완성된 프레임 모음
diff --git a/domain/src/main/java/com/foke/together/domain/output/InternalCameraRepositoryInterface.kt b/domain/src/main/java/com/foke/together/domain/output/InternalCameraRepositoryInterface.kt
index 3e3866e..11053ce 100644
--- a/domain/src/main/java/com/foke/together/domain/output/InternalCameraRepositoryInterface.kt
+++ b/domain/src/main/java/com/foke/together/domain/output/InternalCameraRepositoryInterface.kt
@@ -5,20 +5,22 @@ import android.graphics.Bitmap
import androidx.annotation.IntRange
import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageAnalysis
-import androidx.camera.core.ImageCapture
import androidx.camera.view.PreviewView
import androidx.lifecycle.LifecycleOwner
interface InternalCameraRepositoryInterface {
- suspend fun capture(context: Context): Result
- suspend fun showCameraPreview(
+ suspend fun capture(
+ context: Context,
+ fileName : String,
+ )
+ suspend fun initial(
context: Context,
lifecycleOwner: LifecycleOwner,
- previewView: PreviewView,
+ previewView : PreviewView,
selector : CameraSelector,
- imageAnalysis: ImageAnalysis?,
@IntRange(from = 0, to = 2) captureMode: Int,
@IntRange(from = 0, to = 3) flashMode: Int,
- @IntRange(from = -1, to = 1) aspectRatio: Int
+ imageAnalyzer: ImageAnalysis.Analyzer?,
)
+ suspend fun release(context: Context)
}
\ No newline at end of file
diff --git a/external/src/main/java/com/foke/together/external/repository/ImageRepository.kt b/external/src/main/java/com/foke/together/external/repository/ImageRepository.kt
index 7e86701..fb62d23 100644
--- a/external/src/main/java/com/foke/together/external/repository/ImageRepository.kt
+++ b/external/src/main/java/com/foke/together/external/repository/ImageRepository.kt
@@ -4,11 +4,15 @@ import android.content.Context
import android.graphics.Bitmap
import android.graphics.ImageDecoder
import android.net.Uri
+import android.provider.MediaStore
+import com.foke.together.domain.interactor.GetCameraSourceTypeUseCase
+import com.foke.together.domain.interactor.entity.CameraSourceType
import com.foke.together.domain.interactor.entity.CutFrameTypeV1
import com.foke.together.domain.output.ImageRepositoryInterface
import com.foke.together.util.AppPolicy
import com.foke.together.util.ImageFileUtil
import dagger.hilt.android.qualifiers.ApplicationContext
+import kotlinx.coroutines.flow.map
import javax.inject.Inject
class ImageRepository @Inject constructor(
@@ -25,12 +29,53 @@ class ImageRepository @Inject constructor(
return ImageFileUtil.cacheBitmap(context, image, fileName)
}
- override fun getCachedImageUriList(): List {
+ override fun getCachedImageUriList(sourceType: CameraSourceType): List {
var uriList = mutableListOf()
- context.cacheDir.listFiles().forEach {
- if(it.name.contains(AppPolicy.CAPTURED_FOUR_CUT_IMAGE_NAME)){
- // capture로 시작하는 파일만 반환
- uriList.add(Uri.fromFile(it))
+ when(sourceType){
+ CameraSourceType.EXTERNAL -> {
+ context.cacheDir.listFiles().forEach {
+ if(it.name.contains(AppPolicy.CAPTURED_FOUR_CUT_IMAGE_NAME)){
+ // capture로 시작하는 파일만 반환
+ uriList.add(Uri.fromFile(it))
+ }
+ }
+ }
+ CameraSourceType.INTERNAL -> {
+ val projection = arrayOf(
+ MediaStore.Images.Media._ID,
+ MediaStore.Images.Media.DISPLAY_NAME,
+ MediaStore.Images.Media.DATE_ADDED
+ )
+ val selection = "${MediaStore.Images.Media.RELATIVE_PATH} LIKE ?"
+ val selectionArgs = arrayOf("%Pictures/4cuts/backup%")
+ val sortOrder = "${MediaStore.Images.Media.DATE_ADDED} DESC"
+
+ val cursor = context.contentResolver.query(
+ MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
+ projection,
+ selection,
+ selectionArgs,
+ sortOrder
+ )
+
+ cursor?.use {
+ val idColumn = it.getColumnIndexOrThrow(MediaStore.Images.Media._ID)
+ val latestUris = mutableListOf()
+
+ var count = 0
+ while (it.moveToNext() && count < AppPolicy.CAPTURE_COUNT) {
+ val id = it.getLong(idColumn)
+ val contentUri = Uri.withAppendedPath(
+ MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
+ id.toString()
+ )
+ latestUris.add(contentUri)
+ count++
+ }
+
+ latestUris.reverse()
+ uriList.addAll(latestUris)
+ }
}
}
return uriList
diff --git a/external/src/main/java/com/foke/together/external/repository/InternalCameraRepository.kt b/external/src/main/java/com/foke/together/external/repository/InternalCameraRepository.kt
index 832068e..b38c179 100644
--- a/external/src/main/java/com/foke/together/external/repository/InternalCameraRepository.kt
+++ b/external/src/main/java/com/foke/together/external/repository/InternalCameraRepository.kt
@@ -1,7 +1,10 @@
package com.foke.together.external.repository
+import android.content.ContentValues
import android.content.Context
import android.graphics.Bitmap
+import android.os.Build
+import android.provider.MediaStore
import androidx.annotation.IntRange
import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageAnalysis
@@ -11,85 +14,101 @@ import androidx.camera.core.ImageCaptureException
import androidx.camera.core.ImageProxy
import androidx.camera.core.Preview
import androidx.camera.lifecycle.ProcessCameraProvider
+import androidx.camera.view.LifecycleCameraController
import androidx.camera.view.PreviewView
import androidx.core.content.ContextCompat
import androidx.lifecycle.LifecycleOwner
import com.foke.together.domain.output.InternalCameraRepositoryInterface
import com.foke.together.util.AppLog
import javax.inject.Inject
+import javax.inject.Singleton
+@Singleton
class InternalCameraRepository @Inject constructor(
): InternalCameraRepositoryInterface{
+ private lateinit var previewView: PreviewView
+ private lateinit var cameraController: LifecycleCameraController
private lateinit var imageCapture: ImageCapture
- private lateinit var cameraProvider : ProcessCameraProvider
+
+ private var imageBitmap : Bitmap? = null
override suspend fun capture(
context: Context,
- ): Result {
- var imageBitmap : Bitmap? = null
- imageCapture.takePicture(
- ContextCompat.getMainExecutor(context),
- object : ImageCapture.OnImageCapturedCallback() {
- override fun onCaptureSuccess(imageProxy: ImageProxy) {
- imageBitmap = imageProxy.toBitmap()
- }
-
- override fun onError(exception: ImageCaptureException) {
- imageBitmap = null
- }
+ fileName : String,
+ ) {
+ if(::cameraController.isInitialized) {
+ val contentValues = ContentValues().apply {
+ put(MediaStore.MediaColumns.DISPLAY_NAME, "${fileName}.jpg")
+ put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
+ put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/4cuts/backup")
}
- )
- return if(imageBitmap != null){
- Result.success(imageBitmap!!)
- } else{
- Result.failure(Exception("Unknown error"))
+ val outputOptions = ImageCapture.OutputFileOptions.Builder(
+ context.contentResolver,
+ MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues
+ ).build()
+
+ cameraController.takePicture(
+ outputOptions, ContextCompat.getMainExecutor(context),
+ object : ImageCapture.OnImageSavedCallback {
+ override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
+ if (outputFileResults.savedUri != null) {
+ imageBitmap = MediaStore.Images.Media.getBitmap(
+ context.contentResolver,
+ outputFileResults.savedUri
+ )
+ }
+ }
+ override fun onError(exception: ImageCaptureException) {
+ AppLog.e(TAG, "onError", "Photo capture failed: ${exception.message}")
+ }
+ })
+ }
+ else{
+ throw Exception("cameraController is not initialized")
}
}
- override suspend fun showCameraPreview(
+ override suspend fun initial(
context: Context,
lifecycleOwner: LifecycleOwner,
- previewView: PreviewView,
+ previewView : PreviewView,
selector : CameraSelector,
- imageAnalysis: ImageAnalysis?,
@IntRange(from = 0, to = 2) captureMode: Int,
@IntRange(from = 0, to = 3) flashMode: Int,
- @IntRange(from = -1, to = 1) aspectRatio: Int
+ imageAnalyzer: ImageAnalysis.Analyzer?,
) {
try{
- cameraProvider = ProcessCameraProvider.getInstance(context).get()
- cameraProvider.unbindAll()
- val preview = Preview.Builder().build().also {
- it.surfaceProvider = previewView.surfaceProvider
+ cameraController = LifecycleCameraController(context)
+ cameraController.cameraSelector = selector
+ if(imageAnalyzer != null){
+ cameraController.setImageAnalysisAnalyzer(ContextCompat.getMainExecutor(context), imageAnalyzer)
}
imageCapture = ImageCapture.Builder()
.setCaptureMode(captureMode)
.setFlashMode(flashMode)
- .setTargetAspectRatio(aspectRatio)
.build()
+ cameraController.imageCaptureIoExecutor = ContextCompat.getMainExecutor(context)
+ cameraController.imageCaptureMode = captureMode
+ cameraController.imageCaptureFlashMode = flashMode
+ cameraController.bindToLifecycle(lifecycleOwner)
+ previewView.controller = cameraController
+ }
+ catch (e: Exception){
+ AppLog.e(TAG,"showCameraPreview", e.message!!)
+ }
+ }
- if(imageAnalysis != null) {
- cameraProvider.bindToLifecycle(
- lifecycleOwner,
- selector,
- preview,
- imageAnalysis,
- imageCapture
- )
- }
- else {
- cameraProvider.bindToLifecycle(
- lifecycleOwner,
- selector,
- preview,
- imageCapture
- )
+ override suspend fun release(context: Context) {
+ try{
+ if(::cameraController.isInitialized) {
+ cameraController.unbind()
}
}
catch (e: Exception){
- AppLog.e(TAG,"showCameraPreview", e.message!!)
+ AppLog.e(TAG,"release", e.message!!)
}
}
+
companion object {
private val TAG = InternalCameraRepository::class.java.simpleName
}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 61d474b..8056bc4 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -70,6 +70,9 @@ qrcode-kotlin = "4.5.0"
# camerax --------
camerax = "1.5.0"
+# permission -----
+accompanist = "0.37.3"
+
# test -----------
junit = "4.13.2"
junit-version = "1.3.0"
@@ -143,6 +146,9 @@ camerax-lifecycle = { group = "androidx.camera", name = "camera-lifecycle", vers
camerax-view = { group = "androidx.camera", name = "camera-view", version.ref = "camerax" }
camerax-extensions = { group = "androidx.camera", name = "camera-extensions", version.ref = "camerax" }
+# permission ---
+com-google-accompanist-permission = { group = "com.google.accompanist", name = "accompanist-permissions", version.ref = "accompanist" }
+
# test -----------
junit = { group = "junit", name = "junit", version.ref = "junit" }
diff --git a/presenter/build.gradle.kts b/presenter/build.gradle.kts
index 11ce84c..4746940 100644
--- a/presenter/build.gradle.kts
+++ b/presenter/build.gradle.kts
@@ -49,6 +49,9 @@ dependencies {
// reflect
implementation(kotlin("reflect"))
+ // permission
+ implementation(libs.com.google.accompanist.permission)
+
// test
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
diff --git a/presenter/src/main/java/com/foke/together/presenter/navigation/NavGraph.kt b/presenter/src/main/java/com/foke/together/presenter/navigation/NavGraph.kt
index 5f80650..3bb2b2a 100644
--- a/presenter/src/main/java/com/foke/together/presenter/navigation/NavGraph.kt
+++ b/presenter/src/main/java/com/foke/together/presenter/navigation/NavGraph.kt
@@ -1,6 +1,8 @@
package com.foke.together.presenter.navigation
+import android.Manifest
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
@@ -8,13 +10,27 @@ import androidx.navigation.compose.composable
import com.foke.together.presenter.screen.CameraScreen
import com.foke.together.presenter.screen.GenerateImageScreen
import com.foke.together.presenter.screen.HomeScreen
+import com.foke.together.presenter.screen.InternalCameraScreen
+import com.foke.together.presenter.screen.InternalCameraScreenRoot
import com.foke.together.presenter.screen.SelectFrameScreen
import com.foke.together.presenter.screen.SelectMethodScreen
import com.foke.together.presenter.screen.SettingScreen
import com.foke.together.presenter.screen.ShareScreen
+import com.google.accompanist.permissions.ExperimentalPermissionsApi
+import com.google.accompanist.permissions.PermissionStatus
+import com.google.accompanist.permissions.rememberPermissionState
+@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun NavGraph(navController: NavHostController) {
+ val permission = rememberPermissionState(Manifest.permission.CAMERA)
+
+ LaunchedEffect(permission) {
+ if(permission.status != PermissionStatus.Granted) {
+ permission.launchPermissionRequest() // 권한 요청
+ }
+ }
+
NavHost(
navController = navController,
startDestination = NavRoute.Home.path
@@ -24,6 +40,7 @@ fun NavGraph(navController: NavHostController) {
addSelectFrameScreen(navController, this)
addSelectMethodScreen(navController, this)
addCameraScreen(navController, this)
+ addInternalCameraScreen(navController, this)
addGenerateImageScreen(navController, this)
addShareScreen(navController, this)
}
@@ -69,7 +86,7 @@ private fun addSelectMethodScreen(
navGraphBuilder.composable(route = NavRoute.SelectMethod.path) {
SelectMethodScreen(
navigateToCamera = {
- navController.navigate(NavRoute.Camera.path)
+ navController.navigate(NavRoute.InternalCamera.path)
},
popBackStack = {
navController.popBackStack(NavRoute.SelectFrame.path, inclusive = false)
@@ -94,6 +111,22 @@ private fun addCameraScreen(
}
}
+private fun addInternalCameraScreen(
+ navController: NavHostController,
+ navGraphBuilder: NavGraphBuilder
+) {
+ navGraphBuilder.composable(route = NavRoute.InternalCamera.path) {
+ InternalCameraScreenRoot(
+ navigateToGenerateImage = {
+ navController.navigate(NavRoute.GenerateSingleRowImage.path)
+ },
+ popBackStack = {
+ navController.popBackStack(NavRoute.Home.path, inclusive = false)
+ }
+ )
+ }
+}
+
private fun addGenerateImageScreen(
navController: NavHostController,
navGraphBuilder: NavGraphBuilder
diff --git a/presenter/src/main/java/com/foke/together/presenter/navigation/NavRoute.kt b/presenter/src/main/java/com/foke/together/presenter/navigation/NavRoute.kt
index e0aee63..1be42b0 100644
--- a/presenter/src/main/java/com/foke/together/presenter/navigation/NavRoute.kt
+++ b/presenter/src/main/java/com/foke/together/presenter/navigation/NavRoute.kt
@@ -7,6 +7,7 @@ sealed class NavRoute(val path: String) {
object SelectFrame: NavRoute("select_frame")
object SelectMethod: NavRoute("select_method")
object Camera: NavRoute("camera")
+ object InternalCamera : NavRoute("internal_camera")
object GenerateSingleRowImage: NavRoute("generate_single_row_image")
object GenerateTwoRowImage: NavRoute("generate_two_row_image")
object Share: NavRoute("share")
diff --git a/presenter/src/main/java/com/foke/together/presenter/screen/GenerateImageScreen.kt b/presenter/src/main/java/com/foke/together/presenter/screen/GenerateImageScreen.kt
index e0182a4..f70a48d 100644
--- a/presenter/src/main/java/com/foke/together/presenter/screen/GenerateImageScreen.kt
+++ b/presenter/src/main/java/com/foke/together/presenter/screen/GenerateImageScreen.kt
@@ -13,6 +13,7 @@ import androidx.compose.foundation.layout.width
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
@@ -31,6 +32,7 @@ import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.LifecycleEventEffect
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.foke.together.domain.interactor.entity.DefaultCutFrameSet
import com.foke.together.presenter.R
import com.foke.together.presenter.frame.DefaultCutFrame
@@ -51,6 +53,7 @@ fun GenerateImageScreen(
val graphicsLayer2 = rememberGraphicsLayer()
val coroutineScope = rememberCoroutineScope()
val isFirstState = remember { mutableStateOf(true) }
+ val imageUri by viewModel.imageUri.collectAsStateWithLifecycle()
LifecycleEventEffect(Lifecycle.Event.ON_RESUME) {
AppLog.d(TAG, "LifecycleEventEffect: ON_RESUME", "${viewModel.imageUri}")
@@ -87,13 +90,13 @@ fun GenerateImageScreen(
// TODO: 나중에 다른 CutFrameSet 어떻게 처리해야 할지?
GetDefaultFrame(
cutFrame = viewModel.cutFrame as DefaultCutFrameSet,
- imageUri = viewModel.imageUri,
+ imageUri = imageUri,
graphicsLayer = graphicsLayer1,
)
} else {
GetDefaultFrame(
cutFrame = viewModel.cutFrame as DefaultCutFrameSet,
- imageUri = viewModel.imageUri,
+ imageUri = imageUri,
graphicsLayer = graphicsLayer2,
isForPrint = true,
isPaddingHorizontal = 16.dp
diff --git a/presenter/src/main/java/com/foke/together/presenter/screen/InternalCameraScreen.kt b/presenter/src/main/java/com/foke/together/presenter/screen/InternalCameraScreen.kt
new file mode 100644
index 0000000..87b4527
--- /dev/null
+++ b/presenter/src/main/java/com/foke/together/presenter/screen/InternalCameraScreen.kt
@@ -0,0 +1,169 @@
+package com.foke.together.presenter.screen
+
+import android.content.Context
+import androidx.camera.view.PreviewView
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.material3.BottomAppBar
+import androidx.compose.material3.CenterAlignedTopAppBar
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+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.res.stringResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Devices
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.sp
+import androidx.compose.ui.viewinterop.AndroidView
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.compose.LifecycleEventEffect
+import androidx.lifecycle.compose.LocalLifecycleOwner
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.foke.together.presenter.R
+import com.foke.together.presenter.screen.state.InternalCameraState
+import com.foke.together.presenter.theme.FourCutTogetherTheme
+import com.foke.together.presenter.viewmodel.InternelCameraViewModel
+import com.foke.together.util.AppLog
+
+private val TAG = "InternalCameraScreen"
+
+/*TODO(CameraScreen, InternalCameraScreen 합치기) */
+@Composable
+fun InternalCameraScreenRoot(
+ navigateToGenerateImage: () -> Unit,
+ popBackStack: () -> Unit,
+ viewModel: InternelCameraViewModel = hiltViewModel()
+){
+ val state = viewModel.state
+ val lifecycleOwner = LocalLifecycleOwner.current
+ val context = LocalContext.current
+ val captureCount by viewModel.captureCount.collectAsStateWithLifecycle()
+ val progress by viewModel.progressState.collectAsStateWithLifecycle()
+ LifecycleEventEffect(Lifecycle.Event.ON_START) {
+ viewModel.setCaptureTimer(context) { navigateToGenerateImage() }
+ AppLog.d(TAG, "LifecycleEventEffect. ON_START", "")
+ }
+ LifecycleEventEffect(Lifecycle.Event.ON_RESUME) {
+ AppLog.d(TAG, "LifecycleEventEffect. ON_RESUME", "")
+ viewModel.startCaptureTimer()
+ }
+ LifecycleEventEffect(Lifecycle.Event.ON_STOP) {
+ viewModel.stopCaptureTimer()
+ AppLog.d(TAG, "LifecycleEventEffect. ON_STOP", "")
+ }
+ InternalCameraScreen(
+ state = state,
+ captureCount = captureCount,
+ progress = progress,
+ initialPreview = { context, previewView ->
+ viewModel.initial(context, lifecycleOwner, previewView)
+ },
+ releasePreview = { context ->
+ viewModel.release(context)
+ }
+ )
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun InternalCameraScreen(
+ state : InternalCameraState,
+ captureCount : Int,
+ progress: Float,
+ initialPreview : (Context, PreviewView) -> Unit,
+ releasePreview : (Context) -> Unit,
+){
+ Scaffold(
+ modifier = Modifier.fillMaxSize(),
+ topBar = {
+ CenterAlignedTopAppBar(
+ title = {
+ Text(
+ text = "${captureCount}번째 촬영",
+ fontWeight = FontWeight.Bold,
+ fontSize = 24.sp,
+ textAlign = TextAlign.Center
+ )
+ }
+ )
+ },
+ bottomBar = {
+ BottomAppBar { }
+ }
+ ){ paddingValues ->
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(
+ top = paddingValues.calculateTopPadding(),
+ bottom = paddingValues.calculateBottomPadding()
+ ),
+ verticalArrangement = Arrangement.SpaceEvenly,
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ){
+ Text(
+ modifier = Modifier.fillMaxWidth().wrapContentHeight(),
+ text = stringResource(id = R.string.camera_exclamation_text),
+ fontWeight = FontWeight.Bold,
+ fontSize = 24.sp,
+ textAlign = TextAlign.Center
+ )
+ Box(
+ modifier = Modifier.aspectRatio(1.5f).fillMaxHeight()
+ ) {
+ AndroidView(
+ modifier = Modifier.align(Alignment.Center).fillMaxSize(),
+ factory = { context ->
+ PreviewView(context).also { preview ->
+ initialPreview(context, preview)
+ }
+ },
+ onRelease = { previewView ->
+ releasePreview(previewView.context)
+ }
+
+ )
+ }
+ }
+ }
+}
+
+@Preview(
+ device = Devices.TABLET,
+ showBackground = true,
+)
+@Composable
+fun InternalScreenPreview(){
+ val state by remember { mutableStateOf(InternalCameraState()) }
+ val captureCount by remember{ mutableStateOf(1) }
+ val progress by remember{ mutableStateOf(1f)}
+ FourCutTogetherTheme() {
+ InternalCameraScreen(
+ state = state,
+ captureCount = captureCount,
+ progress = progress,
+ initialPreview = { context, previewView ->
+
+ },
+ releasePreview = { context ->
+
+ }
+ )
+ }
+}
\ No newline at end of file
diff --git a/presenter/src/main/java/com/foke/together/presenter/screen/state/InternalCameraState.kt b/presenter/src/main/java/com/foke/together/presenter/screen/state/InternalCameraState.kt
new file mode 100644
index 0000000..7758f83
--- /dev/null
+++ b/presenter/src/main/java/com/foke/together/presenter/screen/state/InternalCameraState.kt
@@ -0,0 +1,9 @@
+package com.foke.together.presenter.screen.state
+
+import androidx.compose.runtime.Stable
+
+@Stable
+data class InternalCameraState(
+ val aspectRatio : Float = 1.5f,
+ val cutCount : Int = 4,
+)
diff --git a/presenter/src/main/java/com/foke/together/presenter/viewmodel/GenerateImageViewModel.kt b/presenter/src/main/java/com/foke/together/presenter/viewmodel/GenerateImageViewModel.kt
index ba69db4..e9542f7 100644
--- a/presenter/src/main/java/com/foke/together/presenter/viewmodel/GenerateImageViewModel.kt
+++ b/presenter/src/main/java/com/foke/together/presenter/viewmodel/GenerateImageViewModel.kt
@@ -2,22 +2,35 @@ package com.foke.together.presenter.viewmodel
import androidx.compose.ui.graphics.layer.GraphicsLayer
import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
import com.foke.together.domain.interactor.GenerateImageFromViewUseCase
import com.foke.together.domain.interactor.GeneratePhotoFrameUseCaseV1
+import com.foke.together.domain.interactor.GetCameraSourceTypeUseCase
import com.foke.together.domain.interactor.entity.CutFrame
import com.foke.together.domain.interactor.session.GetCurrentSessionUseCase
import com.foke.together.util.AppLog
import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
import javax.inject.Inject
@HiltViewModel
class GenerateImageViewModel @Inject constructor(
- getCurrentSessionUseCase: GetCurrentSessionUseCase,
+ private val getCameraSourceTypeUseCase : GetCameraSourceTypeUseCase,
+ private val getCurrentSessionUseCase: GetCurrentSessionUseCase,
private val generateImageFromViewUseCase: GenerateImageFromViewUseCase,
- generatePhotoFrameUseCaseV1: GeneratePhotoFrameUseCaseV1,
+ private val generatePhotoFrameUseCaseV1: GeneratePhotoFrameUseCaseV1,
): ViewModel() {
val cutFrame: CutFrame = getCurrentSessionUseCase()?.cutFrame ?: run { throw Exception("invalid cut frame") }
- val imageUri = generatePhotoFrameUseCaseV1.getCapturedImageListUri()
+
+ val imageUri = getCameraSourceTypeUseCase().map{ sourceType ->
+ generatePhotoFrameUseCaseV1.getCapturedImageListUri(sourceType)
+ }.stateIn(
+ scope = viewModelScope,
+ started = SharingStarted.WhileSubscribed(5000),
+ initialValue = listOf()
+ )
suspend fun generateImage(graphicsLayer: GraphicsLayer) {
val finalCachedImageUri = generateImageFromViewUseCase(graphicsLayer, isCutFrameForPrint = false)
diff --git a/presenter/src/main/java/com/foke/together/presenter/viewmodel/InternelCameraViewModel.kt b/presenter/src/main/java/com/foke/together/presenter/viewmodel/InternelCameraViewModel.kt
index dbc8716..0c7a64c 100644
--- a/presenter/src/main/java/com/foke/together/presenter/viewmodel/InternelCameraViewModel.kt
+++ b/presenter/src/main/java/com/foke/together/presenter/viewmodel/InternelCameraViewModel.kt
@@ -1,20 +1,31 @@
package com.foke.together.presenter.viewmodel
import android.content.Context
-import androidx.camera.core.AspectRatio
+import android.os.CountDownTimer
import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageCapture
import androidx.camera.view.PreviewView
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.setValue
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
-import com.foke.together.domain.interactor.CaptureWithInternalCameraUseCase
-import com.foke.together.domain.interactor.GetInternalCameraAspectRatioUseCase
+import com.foke.together.domain.interactor.GeneratePhotoFrameUseCaseV1
import com.foke.together.domain.interactor.GetInternalCameraCaptureModeUseCase
import com.foke.together.domain.interactor.GetInternalCameraFlashModeUseCase
import com.foke.together.domain.interactor.GetInternalCameraLensFacingUseCase
-import com.foke.together.domain.interactor.GetInternalCameraPreviewUseCase
+import com.foke.together.domain.interactor.InternalCameraUseCase
+import com.foke.together.domain.interactor.session.GetCurrentSessionUseCase
+import com.foke.together.presenter.screen.state.InternalCameraState
+import com.foke.together.util.AppLog
+import com.foke.together.util.AppPolicy
+import com.foke.together.util.AppPolicy.CAPTURE_INTERVAL
+import com.foke.together.util.AppPolicy.COUNTDOWN_INTERVAL
+import com.foke.together.util.SoundUtil
import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
@@ -22,13 +33,19 @@ import javax.inject.Inject
@HiltViewModel
class InternelCameraViewModel @Inject constructor(
- private val getInternalCameraPreviewUseCase: GetInternalCameraPreviewUseCase,
+ private val internalCameraUseCase: InternalCameraUseCase,
private val getInternalCameraLensFacingUseCase: GetInternalCameraLensFacingUseCase,
private val getInternalCameraFlashModeUseCase: GetInternalCameraFlashModeUseCase,
private val getInternalCameraCaptureModeUseCase: GetInternalCameraCaptureModeUseCase,
- private val getInternalCameraAspectRatioUseCase: GetInternalCameraAspectRatioUseCase,
- private val captureWithInternalCameraUseCase: CaptureWithInternalCameraUseCase
+ private val getSessionUseCase : GetCurrentSessionUseCase,
+ private val generatePhotoFrameUseCaseV1: GeneratePhotoFrameUseCaseV1,
): ViewModel() {
+
+ // TODO(MutableStateFlow 로 처리하기)
+ val captureCount = MutableStateFlow(1)
+ val progressState = MutableStateFlow(1f)
+ private var captureTimer: CountDownTimer? = null
+ private var mTimerState = false
private val cameraSelector = getInternalCameraLensFacingUseCase().stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(),
@@ -44,31 +61,96 @@ class InternelCameraViewModel @Inject constructor(
private val flashMode = getInternalCameraFlashModeUseCase().stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(subscribeDuration),
- initialValue = ImageCapture.FLASH_MODE_ON
+ initialValue = ImageCapture.FLASH_MODE_OFF
)
- private val aspectRatio = getInternalCameraAspectRatioUseCase().stateIn(
- scope = viewModelScope,
- started = SharingStarted.WhileSubscribed(subscribeDuration),
- initialValue = AspectRatio.RATIO_DEFAULT
+
+ private val curFrame = getSessionUseCase()?.cutFrame ?: run { throw Exception("invalid cut frame") }
+
+ val state = InternalCameraState(
+ aspectRatio = curFrame.photoPosition.first().width.toFloat() / curFrame.height.toFloat(),
+ cutCount = curFrame.cutCount,
)
- fun preview(
+ fun initial(
context: Context,
lifecycleOwner: LifecycleOwner,
previewView: PreviewView,
) = viewModelScope.launch {
- getInternalCameraPreviewUseCase(
- context,
- previewView,
- lifecycleOwner,
- cameraSelector.value,
- null,
- captureMode.value,
- flashMode.value,
- aspectRatio.value
+ internalCameraUseCase.initial(
+ context = context,
+ lifecycleOwner = lifecycleOwner,
+ cameraSelector = cameraSelector.value,
+ imageAnalyzer = null,
+ captureMode = captureMode.value,
+ flashMode = flashMode.value,
+ previewView = previewView
)
+ }
+
+ fun release(
+ context: Context,
+ )= viewModelScope.launch{
+ internalCameraUseCase.release(context)
+ }
+
+ fun capture(
+ context: Context,
+ ) = viewModelScope.launch {
+ val fileName = "${AppPolicy.CAPTURED_FOUR_CUT_IMAGE_NAME}_${System.currentTimeMillis()}_${captureCount}"
+ internalCameraUseCase.capture(context, fileName)
+ }
+
+ fun setCaptureTimer(
+ context: Context,
+ nextNavigate : () -> Unit
+ ){
+ if (AppPolicy.isNoCameraDebugMode) {
+ nextNavigate()
+ return
+ }
+
+ viewModelScope.launch {
+ generatePhotoFrameUseCaseV1.clearCapturedImageList()
+ }
+
+ captureTimer = object : CountDownTimer(CAPTURE_INTERVAL, COUNTDOWN_INTERVAL) {
+ override fun onTick(millisUntilFinished: Long) {
+ progressState.value = 1f - (millisUntilFinished.toFloat() / CAPTURE_INTERVAL)
+ }
+ override fun onFinish() {
+ SoundUtil.getCameraSound(context = context )
+ // TODO: 현재 External 실패 시, 스크린 캡쳐 화면을 사용하도록 구성함
+ capture(context)
+ progressState.value = 1f
+ if (captureCount.value < AppPolicy.CAPTURE_COUNT) {
+ AppLog.d(TAG, "captureCount", "$captureCount")
+ captureCount.value += 1
+ start()
+ } else {
+ AppLog.d(TAG, "captureCount", "$captureCount")
+ stopCaptureTimer()
+ captureCount.value = 1
+ nextNavigate()
+ }
+ }
+ }
+ }
+
+ fun startCaptureTimer() = viewModelScope.launch{
+ if(captureTimer != null){
+ if(!mTimerState){
+ mTimerState = true
+ captureTimer!!.start()
+ }
+ }
+ }
+ fun stopCaptureTimer() = viewModelScope.launch{
+ if(captureTimer != null){
+ mTimerState = false
+ captureTimer!!.cancel()
+ }
}
companion object {
diff --git a/presenter/src/main/java/com/foke/together/presenter/viewmodel/ShareViewModel.kt b/presenter/src/main/java/com/foke/together/presenter/viewmodel/ShareViewModel.kt
index 9b0029f..657a6ee 100644
--- a/presenter/src/main/java/com/foke/together/presenter/viewmodel/ShareViewModel.kt
+++ b/presenter/src/main/java/com/foke/together/presenter/viewmodel/ShareViewModel.kt
@@ -70,7 +70,7 @@ class ShareViewModel @Inject constructor(
val result = uploadFileUseCase(sessionKey, singleImageUri.toFile())
AppLog.d(TAG, "generateQRcode" ,"result: $result")
- val downloadUrl: String = getDownloadUrlUseCase(sessionKey).getOrElse { "https://4cuts.store" }
+ val downloadUrl: String = getDownloadUrlUseCase(sessionKey).getOrElse { "https://foke.clon.dev" }
if (AppPolicy.isDebugMode) {
AppLog.e(TAG, "generateQRcode", "sessionKey: $sessionKey")
AppLog.e(TAG, "generateQRcode", "downloadUrl: $downloadUrl")
diff --git a/util/src/main/java/com/foke/together/util/ImageFileUtil.kt b/util/src/main/java/com/foke/together/util/ImageFileUtil.kt
index d94c310..7be0ff9 100644
--- a/util/src/main/java/com/foke/together/util/ImageFileUtil.kt
+++ b/util/src/main/java/com/foke/together/util/ImageFileUtil.kt
@@ -92,7 +92,7 @@ object ImageFileUtil {
putExtra(Intent.EXTRA_STREAM, uri)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
- startActivity(context, createChooser(intent, "Share your image"), null)
+ context.startActivity( createChooser(intent, "Share your image"), null)
}
fun getBitmapFromUri(context: Context, uri: Uri): Bitmap {