diff --git a/.idea/gradle.xml b/.idea/gradle.xml index 30e50480..40aeb355 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -30,6 +30,7 @@ diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 00000000..44ca2d9b --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,41 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 0ad17cbd..8978d23d 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,4 +1,3 @@ - diff --git a/app/build.gradle.kts b/app/build.gradle.kts index bc85d4e6..4fbc54a0 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -22,6 +22,7 @@ dependencies { implementation(project(":feature:login")) implementation(project(":feature:sign-up")) implementation(project(":feature:main")) + implementation(project(":feature:qrcode-scan")) implementation(libs.junit) androidTestImplementation(libs.androidx.test.ext) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 70f3317c..a059ed34 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,6 +3,9 @@ xmlns:tools="http://schemas.android.com/tools"> + + + + suspend fun getOutingList(): Flow> suspend fun getOutingCount(): Flow diff --git a/core/data/src/main/java/com/goms/data/repository/outing/OutingRepositoryImpl.kt b/core/data/src/main/java/com/goms/data/repository/outing/OutingRepositoryImpl.kt index da8180ce..5b71e538 100644 --- a/core/data/src/main/java/com/goms/data/repository/outing/OutingRepositoryImpl.kt +++ b/core/data/src/main/java/com/goms/data/repository/outing/OutingRepositoryImpl.kt @@ -4,11 +4,16 @@ import com.goms.model.response.outing.CountResponse import com.goms.model.response.outing.OutingResponse import com.goms.network.datasource.outing.OutingDataSource import kotlinx.coroutines.flow.Flow +import java.util.UUID import javax.inject.Inject class OutingRepositoryImpl @Inject constructor( private val remoteOutingDataSource: OutingDataSource ) : OutingRepository { + override suspend fun outing(outingUUID: UUID): Flow { + return remoteOutingDataSource.outing(outingUUID) + } + override suspend fun getOutingList(): Flow> { return remoteOutingDataSource.getOutingList() } diff --git a/core/design-system/src/main/java/com/goms/design_system/icon/GomsIcon.kt b/core/design-system/src/main/java/com/goms/design_system/icon/GomsIcon.kt index b722fcdf..8f8813e8 100644 --- a/core/design-system/src/main/java/com/goms/design_system/icon/GomsIcon.kt +++ b/core/design-system/src/main/java/com/goms/design_system/icon/GomsIcon.kt @@ -91,6 +91,17 @@ fun QrScanIcon( ) } +@Composable +fun QrScanGuideIcon( + modifier: Modifier = Modifier +) { + Image( + painter = painterResource(id = R.drawable.ic_qr_scan_guide), + contentDescription = "Qr Scan Guide Icon", + modifier = modifier + ) +} + @Composable fun QrCreateIcon( modifier: Modifier = Modifier diff --git a/core/design-system/src/main/res/drawable/ic_qr_scan_guide.xml b/core/design-system/src/main/res/drawable/ic_qr_scan_guide.xml new file mode 100644 index 00000000..e94db7fb --- /dev/null +++ b/core/design-system/src/main/res/drawable/ic_qr_scan_guide.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/core/domain/src/main/java/com/goms/domain/outing/OutingUseCase.kt b/core/domain/src/main/java/com/goms/domain/outing/OutingUseCase.kt new file mode 100644 index 00000000..9543767e --- /dev/null +++ b/core/domain/src/main/java/com/goms/domain/outing/OutingUseCase.kt @@ -0,0 +1,14 @@ +package com.goms.domain.outing + +import com.goms.data.repository.outing.OutingRepository +import kotlinx.coroutines.flow.Flow +import java.util.UUID +import javax.inject.Inject + +class OutingUseCase @Inject constructor( + private val outingRepository: OutingRepository +) { + suspend operator fun invoke(outingUUID: UUID) = kotlin.runCatching { + outingRepository.outing(outingUUID) + } +} \ No newline at end of file diff --git a/core/network/src/main/java/com/goms/network/api/OutingAPI.kt b/core/network/src/main/java/com/goms/network/api/OutingAPI.kt index b757be82..b52ec744 100644 --- a/core/network/src/main/java/com/goms/network/api/OutingAPI.kt +++ b/core/network/src/main/java/com/goms/network/api/OutingAPI.kt @@ -2,10 +2,19 @@ package com.goms.network.api import com.goms.model.response.outing.CountResponse import com.goms.model.response.outing.OutingResponse +import retrofit2.Response import retrofit2.http.GET +import retrofit2.http.POST +import retrofit2.http.Path import retrofit2.http.Query +import java.util.UUID interface OutingAPI { + @POST("/api/v2/outing/{outingUUID}") + suspend fun outing( + @Path("outingUUID") outingUUID: UUID + ): Response + @GET("/api/v2/outing/") suspend fun getOutingList(): List diff --git a/core/network/src/main/java/com/goms/network/datasource/outing/OutingDataSource.kt b/core/network/src/main/java/com/goms/network/datasource/outing/OutingDataSource.kt index 772c7645..252e7486 100644 --- a/core/network/src/main/java/com/goms/network/datasource/outing/OutingDataSource.kt +++ b/core/network/src/main/java/com/goms/network/datasource/outing/OutingDataSource.kt @@ -3,8 +3,11 @@ package com.goms.network.datasource.outing import com.goms.model.response.outing.CountResponse import com.goms.model.response.outing.OutingResponse import kotlinx.coroutines.flow.Flow +import java.util.UUID interface OutingDataSource { + suspend fun outing(outingUUID: UUID): Flow + suspend fun getOutingList(): Flow> suspend fun getOutingCount(): Flow diff --git a/core/network/src/main/java/com/goms/network/datasource/outing/OutingDataSourceImpl.kt b/core/network/src/main/java/com/goms/network/datasource/outing/OutingDataSourceImpl.kt index d2bb41e4..c33f2830 100644 --- a/core/network/src/main/java/com/goms/network/datasource/outing/OutingDataSourceImpl.kt +++ b/core/network/src/main/java/com/goms/network/datasource/outing/OutingDataSourceImpl.kt @@ -8,11 +8,20 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn +import java.util.UUID import javax.inject.Inject class OutingDataSourceImpl @Inject constructor( private val outingAPI: OutingAPI ) : OutingDataSource { + override suspend fun outing(outingUUID: UUID): Flow = flow { + emit( + GomsApiHandler() + .httpRequest { outingAPI.outing(outingUUID) } + .sendRequest() + ) + }.flowOn(Dispatchers.IO) + override suspend fun getOutingList(): Flow> = flow { emit( GomsApiHandler>() diff --git a/feature/main/src/main/java/com/goms/main/MainScreen.kt b/feature/main/src/main/java/com/goms/main/MainScreen.kt index 30b61292..4198ff3a 100644 --- a/feature/main/src/main/java/com/goms/main/MainScreen.kt +++ b/feature/main/src/main/java/com/goms/main/MainScreen.kt @@ -1,5 +1,6 @@ package com.goms.main +import android.util.Log import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -37,7 +38,8 @@ fun MainRoute( viewModelStoreOwner: ViewModelStoreOwner, onOutingStatusClick: () -> Unit, onLateListClick: () -> Unit, - onStudentManagementClick: () -> Unit + onStudentManagementClick: () -> Unit, + onQrcodeClick: () -> Unit ) { MainViewModelProvider(viewModelStoreOwner = viewModelStoreOwner) { viewModel -> val role by viewModel.role.collectAsStateWithLifecycle(initialValue = "") @@ -55,6 +57,7 @@ fun MainRoute( onOutingStatusClick = onOutingStatusClick, onLateListClick = onLateListClick, onStudentManagementClick = onStudentManagementClick, + onQrcodeClick = onQrcodeClick, mainCallBack = { viewModel.getProfile() viewModel.getLateRankList() @@ -74,6 +77,7 @@ fun MainScreen( onOutingStatusClick: () -> Unit, onLateListClick: () -> Unit, onStudentManagementClick: () -> Unit, + onQrcodeClick: () -> Unit, mainCallBack: () -> Unit ) { LaunchedEffect(true) { @@ -125,7 +129,9 @@ fun MainScreen( .align(Alignment.BottomEnd) .padding(end = 16.dp, bottom = 16.dp), role = role - ) {} + ) { + onQrcodeClick() + } } } } \ No newline at end of file diff --git a/feature/main/src/main/java/com/goms/main/navigation/MainNavigation.kt b/feature/main/src/main/java/com/goms/main/navigation/MainNavigation.kt index 823f7f6c..50edce12 100644 --- a/feature/main/src/main/java/com/goms/main/navigation/MainNavigation.kt +++ b/feature/main/src/main/java/com/goms/main/navigation/MainNavigation.kt @@ -23,14 +23,16 @@ fun NavGraphBuilder.mainScreen( viewModelStoreOwner: ViewModelStoreOwner, onOutingStatusClick: () -> Unit, onLateListClick: () -> Unit, - onStudentManagementClick: () -> Unit + onStudentManagementClick: () -> Unit, + onQrcodeClick: () -> Unit ) { composable(route = mainRoute) { MainRoute( viewModelStoreOwner = viewModelStoreOwner, onOutingStatusClick = onOutingStatusClick, onLateListClick = onLateListClick, - onStudentManagementClick = onStudentManagementClick + onStudentManagementClick = onStudentManagementClick, + onQrcodeClick = onQrcodeClick ) } } diff --git a/feature/qrcode-scan/.gitignore b/feature/qrcode-scan/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/feature/qrcode-scan/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature/qrcode-scan/build.gradle.kts b/feature/qrcode-scan/build.gradle.kts new file mode 100644 index 00000000..2abe4ea7 --- /dev/null +++ b/feature/qrcode-scan/build.gradle.kts @@ -0,0 +1,22 @@ +import com.goms.goms_android_v2.libs + +plugins { + id("goms.android.feature") + id("goms.android.hilt") +} + +android { + namespace = "com.goms.qrcode_scan" +} + +dependencies { + add("implementation", libs.findLibrary("camera.core").get()) + add("implementation", libs.findLibrary("camera.view").get()) + add("implementation", libs.findLibrary("camera.camera2").get()) + add("implementation", libs.findLibrary("camera.lifecycle").get()) + add("implementation", libs.findLibrary("camera.extensions").get()) + + add("implementation", libs.findLibrary("mlkit").get()) + + add("implementation", libs.findLibrary("accompanist.permission").get()) +} diff --git a/feature/qrcode-scan/consumer-rules.pro b/feature/qrcode-scan/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/feature/qrcode-scan/proguard-rules.pro b/feature/qrcode-scan/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/feature/qrcode-scan/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/feature/qrcode-scan/src/androidTest/java/com/goms/qrcode_scan/ExampleInstrumentedTest.kt b/feature/qrcode-scan/src/androidTest/java/com/goms/qrcode_scan/ExampleInstrumentedTest.kt new file mode 100644 index 00000000..fba4f85f --- /dev/null +++ b/feature/qrcode-scan/src/androidTest/java/com/goms/qrcode_scan/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.goms.qrcode_scan + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.goms.qrcode_scan.test", appContext.packageName) + } +} \ No newline at end of file diff --git a/feature/qrcode-scan/src/main/AndroidManifest.xml b/feature/qrcode-scan/src/main/AndroidManifest.xml new file mode 100644 index 00000000..a5918e68 --- /dev/null +++ b/feature/qrcode-scan/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/feature/qrcode-scan/src/main/java/com/goms/qrcode_scan/QrcodeScanScreen.kt b/feature/qrcode-scan/src/main/java/com/goms/qrcode_scan/QrcodeScanScreen.kt new file mode 100644 index 00000000..b9f7b7ae --- /dev/null +++ b/feature/qrcode-scan/src/main/java/com/goms/qrcode_scan/QrcodeScanScreen.kt @@ -0,0 +1,80 @@ +package com.goms.qrcode_scan + +import android.Manifest +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.layout.width +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import com.goms.design_system.theme.GomsTheme +import com.goms.qrcode_scan.component.QrcodeScanGuide +import com.goms.qrcode_scan.component.QrcodeScanPreview +import com.goms.qrcode_scan.component.QrcodeScanTopBar +import com.goms.qrcode_scan.viewmodel.QrcodeViewModel +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.PermissionState +import com.google.accompanist.permissions.isGranted +import com.google.accompanist.permissions.rememberPermissionState +import com.google.accompanist.permissions.shouldShowRationale +import java.util.UUID + +@OptIn(ExperimentalPermissionsApi::class) +@Composable +fun QrcodeScanRoute( + onPermissionBlock: () -> Unit, + viewModel: QrcodeViewModel = hiltViewModel(), +) { + val cameraPermissionState: PermissionState = rememberPermissionState(Manifest.permission.CAMERA) + + LaunchedEffect("getPermission") { + if (!cameraPermissionState.status.isGranted && !cameraPermissionState.status.shouldShowRationale) run { + cameraPermissionState.launchPermissionRequest() + } + } + + if (cameraPermissionState.status.isGranted) { + QrcodeScanScreen( + onQrcodeScan = { qrcodeData -> + viewModel.outing(UUID.fromString(qrcodeData)) + } + ) + } else { + onPermissionBlock() + } +} + +@androidx.annotation.OptIn(androidx.camera.core.ExperimentalGetImage::class) +@Composable +fun QrcodeScanScreen( + onQrcodeScan: (String?) -> Unit +) { + GomsTheme { _, _ -> + QrcodeScanPreview( + context = LocalContext.current, + onQrcodeScan = { qrcodeData -> + onQrcodeScan(qrcodeData) + } + ) + Column( + modifier = Modifier + .fillMaxSize() + .statusBarsPadding(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + QrcodeScanTopBar() + Spacer(modifier = Modifier.height(224.dp)) + QrcodeScanGuide() + } + } +} diff --git a/feature/qrcode-scan/src/main/java/com/goms/qrcode_scan/component/QrcodeScanGuide.kt b/feature/qrcode-scan/src/main/java/com/goms/qrcode_scan/component/QrcodeScanGuide.kt new file mode 100644 index 00000000..5eacd038 --- /dev/null +++ b/feature/qrcode-scan/src/main/java/com/goms/qrcode_scan/component/QrcodeScanGuide.kt @@ -0,0 +1,19 @@ +package com.goms.qrcode_scan.component + +import androidx.compose.foundation.layout.Column +import androidx.compose.runtime.Composable +import androidx.compose.ui.tooling.preview.Preview +import com.goms.design_system.theme.GomsTheme +import com.goms.design_system.icon.QrScanGuideIcon +@Composable +fun QrcodeScanGuide() { + GomsTheme { _, _ -> + QrScanGuideIcon() + } +} + +@Composable +@Preview(showBackground = true) +fun QrcodeScanGuidePreview() { + QrcodeScanGuide() +} \ No newline at end of file diff --git a/feature/qrcode-scan/src/main/java/com/goms/qrcode_scan/component/QrcodeScanPreview.kt b/feature/qrcode-scan/src/main/java/com/goms/qrcode_scan/component/QrcodeScanPreview.kt new file mode 100644 index 00000000..9fd4d538 --- /dev/null +++ b/feature/qrcode-scan/src/main/java/com/goms/qrcode_scan/component/QrcodeScanPreview.kt @@ -0,0 +1,88 @@ +package com.goms.qrcode_scan.component + +import android.content.Context +import android.util.Log +import android.view.ViewGroup +import android.widget.LinearLayout +import android.widget.Toast +import androidx.camera.core.CameraSelector +import androidx.camera.core.ImageAnalysis +import androidx.camera.core.ImageCapture +import androidx.camera.core.Preview +import androidx.camera.lifecycle.ProcessCameraProvider +import androidx.camera.view.LifecycleCameraController +import androidx.camera.view.PreviewView +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.compose.ui.viewinterop.AndroidView +import androidx.core.app.ComponentActivity +import androidx.core.content.ContextCompat +import androidx.lifecycle.LifecycleOwner +import com.goms.design_system.theme.GomsTheme +import com.goms.qrcode_scan.util.QrcodeScanner +import java.util.concurrent.Executors + +@androidx.camera.core.ExperimentalGetImage +@Composable +fun QrcodeScanPreview( + context: Context, + onQrcodeScan: (String?) -> Unit +) { + GomsTheme { _, _ -> + Scaffold( + modifier = Modifier.fillMaxSize(), + ) { innerPadding: PaddingValues -> + AndroidView({ context -> + val cameraExecutor = Executors.newSingleThreadExecutor() + val previewView = PreviewView(context).also { + it.scaleType = PreviewView.ScaleType.FILL_CENTER + } + val cameraProviderFuture = ProcessCameraProvider.getInstance(context) + cameraProviderFuture.addListener({ + val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get() + + val preview = Preview.Builder() + .build() + .also { + it.setSurfaceProvider(previewView.surfaceProvider) + } + + val imageCapture = ImageCapture.Builder().build() + + val imageAnalyzer = ImageAnalysis.Builder() + .build() + .also { + it.setAnalyzer(cameraExecutor, QrcodeScanner { qrcodeData -> + Toast.makeText(context, qrcodeData, Toast.LENGTH_SHORT).show() + onQrcodeScan(qrcodeData) + }) + } + + val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA + + try { + cameraProvider.unbindAll() + + cameraProvider.bindToLifecycle( + context as ComponentActivity, cameraSelector, preview, imageCapture, imageAnalyzer) + + } catch(exc: Exception) { + Log.d("qrcode", "binding failed") + } + }, ContextCompat.getMainExecutor(context)) + previewView + }, + modifier = Modifier + .fillMaxSize() + .padding(innerPadding)) + } + } +} \ No newline at end of file diff --git a/feature/qrcode-scan/src/main/java/com/goms/qrcode_scan/component/QrcodeScanTopBar.kt b/feature/qrcode-scan/src/main/java/com/goms/qrcode_scan/component/QrcodeScanTopBar.kt new file mode 100644 index 00000000..644568c4 --- /dev/null +++ b/feature/qrcode-scan/src/main/java/com/goms/qrcode_scan/component/QrcodeScanTopBar.kt @@ -0,0 +1,46 @@ +package com.goms.qrcode_scan.component + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.goms.design_system.component.button.GomsBackButton +import com.goms.design_system.icon.CloseIcon +import com.goms.design_system.icon.GomsTextIcon +import com.goms.design_system.theme.GomsTheme + +@Composable +fun QrcodeScanTopBar( +) { + GomsTheme { color, _ -> + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + GomsTextIcon(tint = color.BLACK) + Box( + modifier = Modifier.size(64.dp, 56.dp), + contentAlignment = Alignment.Center + ) { + CloseIcon(tint = color.BLACK) + } + } + } +} + +@Composable +@Preview(showBackground = true) +fun QrcodeScanTopBarPreview() { + QrcodeScanTopBar() +} \ No newline at end of file diff --git a/feature/qrcode-scan/src/main/java/com/goms/qrcode_scan/navigation/QrcodeNavigation.kt b/feature/qrcode-scan/src/main/java/com/goms/qrcode_scan/navigation/QrcodeNavigation.kt new file mode 100644 index 00000000..87c0572e --- /dev/null +++ b/feature/qrcode-scan/src/main/java/com/goms/qrcode_scan/navigation/QrcodeNavigation.kt @@ -0,0 +1,23 @@ +package com.goms.qrcode_scan.navigation + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavOptions +import androidx.navigation.compose.composable +import com.goms.qrcode_scan.QrcodeScanRoute + +const val qrcodeScanRoute = "qrcode_scan_route" + +fun NavController.navigateToQrScan(navOptions: NavOptions? = null) { + this.navigate(qrcodeScanRoute, navOptions) +} + +fun NavGraphBuilder.qrcodeScanScreen( + onPermissionBlock: () -> Unit +) { + composable(route = qrcodeScanRoute) { + QrcodeScanRoute( + onPermissionBlock = onPermissionBlock + ) + } +} \ No newline at end of file diff --git a/feature/qrcode-scan/src/main/java/com/goms/qrcode_scan/util/QrcodeScanner.kt b/feature/qrcode-scan/src/main/java/com/goms/qrcode_scan/util/QrcodeScanner.kt new file mode 100644 index 00000000..3e1a7a21 --- /dev/null +++ b/feature/qrcode-scan/src/main/java/com/goms/qrcode_scan/util/QrcodeScanner.kt @@ -0,0 +1,38 @@ +package com.goms.qrcode_scan.util + +import android.util.Log +import androidx.camera.core.ImageAnalysis +import androidx.camera.core.ImageProxy +import com.google.mlkit.vision.barcode.BarcodeScannerOptions +import com.google.mlkit.vision.barcode.BarcodeScanning +import com.google.mlkit.vision.barcode.common.Barcode +import com.google.mlkit.vision.common.InputImage + +class QrcodeScanner( + val qrcodeData: (String?) -> Unit +) : ImageAnalysis.Analyzer { + @androidx.annotation.OptIn(androidx.camera.core.ExperimentalGetImage::class) + override fun analyze(imageProxy: ImageProxy) { + val options = BarcodeScannerOptions.Builder() + .setBarcodeFormats(Barcode.FORMAT_QR_CODE) + .build() + + val scanner = BarcodeScanning.getClient(options) + val mediaImage = imageProxy.image + mediaImage?.let { + val image = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees) + + scanner.process(image) + .addOnSuccessListener { barcodes -> + if (barcodes.isNotEmpty()) { + val qrCodeValue = barcodes[0].displayValue + qrcodeData(qrCodeValue) + } + } + .addOnFailureListener { + Log.d("qrcode","scan fail") + } + } + imageProxy.close() + } +} \ No newline at end of file diff --git a/feature/qrcode-scan/src/main/java/com/goms/qrcode_scan/viewmodel/QrcodeViewModel.kt b/feature/qrcode-scan/src/main/java/com/goms/qrcode_scan/viewmodel/QrcodeViewModel.kt new file mode 100644 index 00000000..ab643af9 --- /dev/null +++ b/feature/qrcode-scan/src/main/java/com/goms/qrcode_scan/viewmodel/QrcodeViewModel.kt @@ -0,0 +1,41 @@ +package com.goms.qrcode_scan.viewmodel + +import android.util.Log +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.goms.common.result.Result +import com.goms.common.result.asResult +import com.goms.domain.auth.LoginUseCase +import com.goms.domain.auth.SaveTokenUseCase +import com.goms.domain.outing.OutingUseCase +import com.goms.model.request.auth.SignUpRequest +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch +import java.util.UUID +import javax.inject.Inject + +@HiltViewModel +class QrcodeViewModel @Inject constructor( + private val outingUseCase: OutingUseCase +) : ViewModel() { + private val _outingState = MutableStateFlow>(Result.Loading) + val outingState = _outingState.asStateFlow() + + fun outing(outingUUID: UUID) = viewModelScope.launch { + outingUseCase(outingUUID = outingUUID) + .onSuccess { + it.catch { remoteError -> + _outingState.value = Result.Error(remoteError) + }.collect { result -> + _outingState.value = Result.Success(result) + } + }.onFailure { + _outingState.value = Result.Error(it) + } + } +} \ No newline at end of file diff --git a/feature/qrcode-scan/src/test/java/com/goms/qrcode_scan/ExampleUnitTest.kt b/feature/qrcode-scan/src/test/java/com/goms/qrcode_scan/ExampleUnitTest.kt new file mode 100644 index 00000000..be87efd1 --- /dev/null +++ b/feature/qrcode-scan/src/test/java/com/goms/qrcode_scan/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package com.goms.qrcode_scan + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 05810de4..f9a3a0ec 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,6 @@ [versions] #Define Versions +accompanist = "0.31.2-alpha" androidGradlePlugin = "8.1.4" androidxActivity = "1.9.0-alpha01" androidxAppCompat = "1.6.1" @@ -22,6 +23,8 @@ androidxTestRules = "1.5.0" androidxTestRunner = "1.5.2" androidx-test-ext-junit = "1.1.5" androidxWindowManager = "1.2.0" +camera = "1.3.0-beta01" +cameraRc = "1.3.0-beta01-rc01@aar" coil = "2.4.0" converter-moshi = "2.9.0" firebaseBom = "31.2.0" @@ -41,6 +44,7 @@ ksp = "1.8.10-1.0.9" lint = "31.2.1" lifecycle-runtime-ktx = "2.6.2" material = "1.2.0-alpha02" +mlkit = "17.0.3" moshi = "1.15.0" okhttp = "4.11.0" org-jetbrains-kotlin-android = "1.8.10" @@ -54,6 +58,7 @@ turbine = "1.0.0" [libraries] #Define Library +accompanist-permission = { group = "com.google.accompanist", name = "accompanist-permissions", version.ref = "accompanist" } androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "androidxActivity" } androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "androidxAppCompat" } androidx-browser = { group = "androidx.browser", name = "browser", version.ref = "androidxBrowser" } @@ -83,6 +88,11 @@ androidx-test-ext = { group = "androidx.test.ext", name = "junit-ktx", version.r androidx-test-rules = { group = "androidx.test", name = "rules", version.ref = "androidxTestRules" } androidx-test-runner = { group = "androidx.test", name = "runner", version.ref = "androidxTestRunner" } androidx-window-manager = { module = "androidx.window:window", version.ref = "androidxWindowManager" } +camera-core = { group = "androidx.camera", name = "camera-core", version.ref = "camera" } +camera-view = { group = "androidx.camera", name = "camera-view", version.ref = "camera" } +camera-camera2 = { group = "androidx.camera", name = "camera-camera2", version.ref = "cameraRc" } +camera-lifecycle = { group = "androidx.camera", name = "camera-lifecycle", version.ref = "camera" } +camera-extensions = { group = "androidx.camera", name = "camera-extensions", version.ref = "camera" } coil-kt = { group = "io.coil-kt", name = "coil", version.ref = "coil" } coil-kt-compose = { group = "io.coil-kt", name = "coil-compose", version.ref = "coil" } coil-kt-svg = { group = "io.coil-kt", name = "coil-svg", version.ref = "coil" } @@ -99,6 +109,7 @@ kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-cor kotlinx-datetime = { group = "org.jetbrains.kotlinx", name = "kotlinx-datetime", version.ref = "kotlinxDateTime" } kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } lint-api = { group = "com.android.tools.lint", name = "lint-api", version.ref = "lint" } +mlkit = { group = "com.google.mlkit", name = "barcode-scanning", version.ref = "mlkit" } moshi = { group = "com.squareup.moshi", name = "moshi", version.ref = "moshi" } okhttp-logging = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttp" } protobuf-kotlin-lite = { group = "com.google.protobuf", name = "protobuf-kotlin-lite", version.ref = "protobuf" } diff --git a/settings.gradle.kts b/settings.gradle.kts index ebfd3d38..779683ce 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -34,3 +34,4 @@ include(":core:common") include(":feature:login") include(":feature:sign-up") include(":feature:main") +include(":feature:qrcode-scan")