From 2846e76e94953f466bbbfa431f89abbb72bbecaa Mon Sep 17 00:00:00 2001 From: Mahmudul Hasan Shohag Date: Sat, 21 Dec 2024 01:59:15 +0600 Subject: [PATCH 1/9] feat: add google barcode scanner example --- app/build.gradle.kts | 3 + app/src/main/AndroidManifest.xml | 4 + .../whynotcompose/ui/screens/NavGraphMain.kt | 15 ++ .../barcodescanner/BarcodeScannerScreen.kt | 139 ++++++++++++++++++ .../barcodescanner/BarcodeScannerViewModel.kt | 68 +++++++++ .../ui/screens/tutorial/index/TutorialList.kt | 6 + gradle/libs.versions.toml | 2 + 7 files changed, 237 insertions(+) create mode 100644 app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/BarcodeScannerScreen.kt create mode 100644 app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/BarcodeScannerViewModel.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 0e92ad4..677be8e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -250,6 +250,9 @@ dependencies { implementation(platform(libs.firebase.bom)) implementation(libs.firebase.analytics.ktx) + + // Code Scanner + implementation(libs.google.playservice.code.scanner) } baselineProfile { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index c386a4c..1b74fba 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -121,6 +121,10 @@ android:name="com.google.android.geo.API_KEY" android:value="${MAPS_API_KEY}" /> + + Unit +) { + val scannedCode = viewModel.scannedCode.collectAsState() + + BarcodeScannerScreenSkeleton( + scannedCode = scannedCode.value, + goBack = goBack, + onOpenGoogleCodeScannerClick = { + viewModel.openGoogleBarcodeScanner() + }, + onOpenCustomCodeScannerClick = { + // TODO: implement custom code scanner + } + ) +} + +@PreviewLightDark +@Composable +private fun BarcodeScannerScreenSkeletonPreview() { + AppTheme { + BarcodeScannerScreenSkeleton() + } +} + +@Suppress("ktlint:compose:modifier-missing-check") +@Composable +fun BarcodeScannerScreenSkeleton( + scannedCode: String? = null, + goBack: () -> Unit = {}, + onOpenGoogleCodeScannerClick: () -> Unit = {}, + onOpenCustomCodeScannerClick: () -> Unit = {} +) { + Scaffold( + Modifier + .navigationBarsPadding() + .imePadding() + .statusBarsPadding(), + topBar = { + AppComponent.Header( + "BarcodeScanner", + goBack = goBack + ) + } + ) { innerPadding -> + Column( + Modifier + .padding(innerPadding) + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(start = 16.dp, end = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text( + "Scanned Code", + style = MaterialTheme.typography.titleMedium + ) + + if (scannedCode.isNullOrBlank()) { + Text("No Result") + } else { + Text(scannedCode) + } + + Spacer(Modifier.height(32.dp)) + + Button( + onClick = onOpenGoogleCodeScannerClick + ) { + Text("Open Google Code Scanner") + } + + Spacer(Modifier.height(16.dp)) + + Button( + onClick = onOpenCustomCodeScannerClick + ) { + Text("Open Custom Code Scanner") + } + } + } +} diff --git a/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/BarcodeScannerViewModel.kt b/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/BarcodeScannerViewModel.kt new file mode 100644 index 0000000..6a72ad6 --- /dev/null +++ b/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/BarcodeScannerViewModel.kt @@ -0,0 +1,68 @@ +/* + * Copyright 2021 Md. Mahmudul Hasan Shohag + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ------------------------------------------------------------------------ + * + * Project: Why Not Compose! + * Developed by: @ImaginativeShohag + * + * Md. Mahmudul Hasan Shohag + * imaginativeshohag@gmail.com + * + * Source: https://github.com/ImaginativeShohag/Why-Not-Compose + */ + +package org.imaginativeworld.whynotcompose.ui.screens.tutorial.barcodescanner + +import android.content.Context +import androidx.lifecycle.ViewModel +import com.google.mlkit.vision.barcode.common.Barcode +import com.google.mlkit.vision.codescanner.GmsBarcodeScannerOptions +import com.google.mlkit.vision.codescanner.GmsBarcodeScanning +import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow + +@HiltViewModel +class BarcodeScannerViewModel @Inject constructor( + @ApplicationContext private val appContext: Context +) : ViewModel() { + private val _scannedCode = MutableStateFlow(null) + val scannedCode = _scannedCode.asStateFlow() + + // ---------------------------------------------------------------- + + fun openGoogleBarcodeScanner() { + val options = GmsBarcodeScannerOptions.Builder() + .setBarcodeFormats(Barcode.FORMAT_ALL_FORMATS) + .enableAutoZoom() + .build() + + val scanner = GmsBarcodeScanning.getClient(appContext, options) + + scanner.startScan() + .addOnSuccessListener { barcode -> + _scannedCode.value = barcode.rawValue + } + .addOnCanceledListener { + // Task canceled + } + .addOnFailureListener { e -> + _scannedCode.value = e.message + } + } +} diff --git a/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/index/TutorialList.kt b/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/index/TutorialList.kt index b2661c7..0d96978 100644 --- a/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/index/TutorialList.kt +++ b/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/index/TutorialList.kt @@ -159,6 +159,12 @@ data class Tutorial( description = "Check the baseline profile install status using `ProfileVerifier`.", route = TutorialsScreen.TutorialBaselineProfiles, level = TutorialLevel.Intermediate + ), + Tutorial( + name = "Barcode Scanner", + description = "Multiple ways to scan barcode.", + route = TutorialsScreen.TutorialBarcodeScanner, + level = TutorialLevel.Intermediate ) ) .sortedBy { it.name } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7e55187..5bae094 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -61,6 +61,7 @@ ucrop = "2.2.10" lottie = "6.6.1" exoplayer = "2.19.1" googlePlayServiceMaps = "19.0.0" +googlePlayServiceCodeScanner = "16.1.0" googleMaps = "5.1.1" googleMapsCompose = "6.4.0" androidxConstraintlayoutCompose = "1.1.0" @@ -84,6 +85,7 @@ google-playservice-maps = { group = "com.google.android.gms", name = "play-servi google-maps-ktx = { group = "com.google.maps.android", name = "maps-ktx", version.ref = "googleMaps" } google-maps-utils-ktx = { group = "com.google.maps.android", name = "maps-utils-ktx", version.ref = "googleMaps" } google-maps-compose = { group = "com.google.maps.android", name = "maps-compose", version.ref = "googleMapsCompose" } +google-playservice-code-scanner = { group = "com.google.android.gms", name = "play-services-code-scanner", version.ref = "googlePlayServiceCodeScanner" } accompanist-permissions = { group = "com.google.accompanist", name = "accompanist-permissions", version.ref = "accompanist" } accompanist-systemuicontroller = { group = "com.google.accompanist", name = "accompanist-systemuicontroller", version.ref = "accompanist" } From 08365889fd89976b572922a3f3885b4bd41cfd48 Mon Sep 17 00:00:00 2001 From: Mahmudul Hasan Shohag Date: Sun, 22 Dec 2024 03:35:05 +0600 Subject: [PATCH 2/9] feat: initial implementation of custom barcode scanning --- app/build.gradle.kts | 10 + app/src/main/AndroidManifest.xml | 7 +- .../whynotcompose/ui/screens/NavGraphMain.kt | 6 +- .../custombarcodescanner/BarcodeAnalyser.kt | 51 +++++ .../custombarcodescanner/CameraPreviewView.kt | 176 ++++++++++++++++++ .../CustomBarcodeScannerSheet.kt | 166 +++++++++++++++++ .../CustomBarcodeScannerViewModel.kt | 72 +++++++ .../{ => index}/BarcodeScannerScreen.kt | 24 ++- .../{ => index}/BarcodeScannerViewModel.kt | 6 +- app/src/main/res/raw/qr_code_scanning.json | 1 + gradle/libs.versions.toml | 18 ++ 11 files changed, 531 insertions(+), 6 deletions(-) create mode 100644 app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/custombarcodescanner/BarcodeAnalyser.kt create mode 100644 app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/custombarcodescanner/CameraPreviewView.kt create mode 100644 app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/custombarcodescanner/CustomBarcodeScannerSheet.kt create mode 100644 app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/custombarcodescanner/CustomBarcodeScannerViewModel.kt rename app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/{ => index}/BarcodeScannerScreen.kt (84%) rename app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/{ => index}/BarcodeScannerViewModel.kt (95%) create mode 100644 app/src/main/res/raw/qr_code_scanning.json diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 677be8e..4bd91c6 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -253,6 +253,16 @@ dependencies { // Code Scanner implementation(libs.google.playservice.code.scanner) + implementation(libs.mlkit.barcode.scanning) + + // Camera + implementation(libs.camerax.core) + implementation(libs.camerax.camera2) + implementation(libs.camerax.lifecycle) + implementation(libs.camerax.video) + implementation(libs.camerax.view) + implementation(libs.camerax.mlkit.vision) + implementation(libs.camerax.extensions) } baselineProfile { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1b74fba..18c9cbc 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -34,6 +34,11 @@ + + + @@ -123,7 +128,7 @@ + android:value="barcode_ui" /> ) -> Unit +) : ImageAnalysis.Analyzer { + override fun analyze(image: ImageProxy) { + image.image?.let { imageToAnalyze -> + val options = BarcodeScannerOptions.Builder() + .setBarcodeFormats(Barcode.FORMAT_ALL_FORMATS) + .setZoomSuggestionOptions( + ZoomSuggestionOptions.Builder { zoomRatio -> + Timber.d("zoom suggestions ratio: $zoomRatio") + controller.setZoomRatio(zoomRatio) + true + }.build() + ) + .build() + + val barcodeScanner = BarcodeScanning.getClient(options) + val imageToProcess = InputImage.fromMediaImage(imageToAnalyze, image.imageInfo.rotationDegrees) + + barcodeScanner.process(imageToProcess) + .addOnSuccessListener { barcodes -> + if (barcodes.isNotEmpty()) { + onBarcodeDetected(barcodes) + } else { + Timber.d("No barcode Scanned") + } + } + .addOnFailureListener { exception -> + Timber.e(exception) + } + .addOnCompleteListener { + image.close() + } + } ?: image.close() + } +} diff --git a/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/custombarcodescanner/CameraPreviewView.kt b/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/custombarcodescanner/CameraPreviewView.kt new file mode 100644 index 0000000..615888c --- /dev/null +++ b/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/custombarcodescanner/CameraPreviewView.kt @@ -0,0 +1,176 @@ +package org.imaginativeworld.whynotcompose.ui.screens.tutorial.barcodescanner.custombarcodescanner + +import android.view.ViewGroup +import androidx.camera.core.CameraSelector +import androidx.camera.core.ImageAnalysis +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.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.FlashOff +import androidx.compose.material.icons.rounded.FlashOn +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +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.tooling.preview.PreviewLightDark +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView +import androidx.core.content.ContextCompat +import androidx.lifecycle.compose.LocalLifecycleOwner +import com.airbnb.lottie.compose.LottieAnimation +import com.airbnb.lottie.compose.LottieCompositionSpec +import com.airbnb.lottie.compose.LottieConstants +import com.airbnb.lottie.compose.rememberLottieComposition +import com.google.common.util.concurrent.ListenableFuture +import com.google.mlkit.vision.barcode.common.Barcode +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors +import org.imaginativeworld.whynotcompose.R +import org.imaginativeworld.whynotcompose.common.compose.theme.AppTheme +import timber.log.Timber + +@Composable +fun CameraPreviewView( + onSuccess: (barcodes: List) -> Unit, + modifier: Modifier = Modifier +) { + val context = LocalContext.current + val lifecycleOwner = LocalLifecycleOwner.current + + val cameraController = remember { + LifecycleCameraController(context).apply { + // Bind the LifecycleCameraController to the lifecycleOwner + bindToLifecycle(lifecycleOwner) + } + } + + AndroidView( + factory = { androidViewContext -> + val previewView = PreviewView(androidViewContext).apply { + setBackgroundColor(android.graphics.Color.GREEN) + this.scaleType = PreviewView.ScaleType.FILL_CENTER + layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + ) + implementationMode = PreviewView.ImplementationMode.COMPATIBLE + controller = cameraController + } + + // ---------------------------------------------------------------- + + val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA + val cameraExecutor: ExecutorService = Executors.newSingleThreadExecutor() + val cameraProviderFuture: ListenableFuture = ProcessCameraProvider.getInstance(context) + + cameraProviderFuture.addListener({ + val preview = Preview.Builder() + .build() + .also { + it.surfaceProvider = previewView.surfaceProvider + } + + val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get() + val barcodeAnalyser = BarcodeAnalyser( + controller = cameraController + ) { barcodes -> + onSuccess(barcodes) + } + val imageAnalysis: ImageAnalysis = ImageAnalysis.Builder() + .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) + .build() + .also { + it.setAnalyzer(cameraExecutor, barcodeAnalyser) + } + + try { + // Unbind use cases before rebinding + cameraProvider.unbindAll() + + // Bind use cases to camera + cameraProvider.bindToLifecycle( + lifecycleOwner, + cameraSelector, + preview, + imageAnalysis + ) + } catch (e: Exception) { + Timber.d("CameraPreview: ${e.localizedMessage}") + } + }, ContextCompat.getMainExecutor(context)) + + // ---------------------------------------------------------------- + + previewView + }, + modifier = modifier, + onRelease = { + cameraController.unbind() + } + ) +} + +@Composable +fun CodeScannerView( + isFlashAvailable: Boolean, + isFlashOn: Boolean, + onFlashToggleClick: () -> Unit, + modifier: Modifier = Modifier, + cameraContent: @Composable () -> Unit +) { + val composition by rememberLottieComposition( + LottieCompositionSpec.RawRes(R.raw.qr_code_scanning) + ) + + Box(modifier = modifier.fillMaxSize()) { + cameraContent() + + LottieAnimation( + modifier = Modifier + .size(250.dp) + .align(Alignment.Center), + composition = composition, + iterations = LottieConstants.IterateForever + ) + + if (isFlashAvailable) { + IconButton( + onClick = onFlashToggleClick, + modifier = Modifier.padding(16.dp) + ) { + Icon( + if (isFlashOn) { + Icons.Rounded.FlashOn + } else { + Icons.Rounded.FlashOff + }, + contentDescription = "Toggle Flash" + ) + } + } + } +} + +@PreviewLightDark +@Composable +private fun CameraPreviewViewPreview() { + AppTheme { + CodeScannerView( + isFlashAvailable = true, + isFlashOn = true, + onFlashToggleClick = {} + ) { + // camera preview view + } + } +} diff --git a/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/custombarcodescanner/CustomBarcodeScannerSheet.kt b/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/custombarcodescanner/CustomBarcodeScannerSheet.kt new file mode 100644 index 0000000..5ad6cdc --- /dev/null +++ b/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/custombarcodescanner/CustomBarcodeScannerSheet.kt @@ -0,0 +1,166 @@ +/* + * Copyright 2021 Md. Mahmudul Hasan Shohag + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ------------------------------------------------------------------------ + * + * Project: Why Not Compose! + * Developed by: @ImaginativeShohag + * + * Md. Mahmudul Hasan Shohag + * imaginativeshohag@gmail.com + * + * Source: https://github.com/ImaginativeShohag/Why-Not-Compose + */ + +package org.imaginativeworld.whynotcompose.ui.screens.tutorial.barcodescanner.custombarcodescanner + +import android.Manifest +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.camera.core.ImageCapture.FLASH_MODE_OFF +import androidx.camera.core.ImageCapture.FLASH_MODE_ON +import androidx.camera.view.LifecycleCameraController +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.tooling.preview.PreviewLightDark +import androidx.lifecycle.compose.LocalLifecycleOwner +import com.google.mlkit.vision.barcode.common.Barcode +import kotlinx.coroutines.launch +import org.imaginativeworld.whynotcompose.base.extensions.toast +import org.imaginativeworld.whynotcompose.base.models.Event +import org.imaginativeworld.whynotcompose.common.compose.theme.AppTheme + +// todo#1: add accompanist permission and add permission uis +// todo#2: add no camera ui + +@Composable +fun CustomBarcodeScannerSheet( + onDismissRequest: () -> Unit, + onSuccess: (barcode: Barcode) -> Unit +) { + val context = LocalContext.current + val lifecycleOwner = LocalLifecycleOwner.current + val currentOnSuccess by rememberUpdatedState(onSuccess) + val scope = rememberCoroutineScope() + val bottomSheetState = rememberModalBottomSheetState( + skipPartiallyExpanded = true + ) + + val goBack: () -> Unit = { + scope.launch { + bottomSheetState.hide() + onDismissRequest() + } + } + + val cameraController = remember { + LifecycleCameraController(context).apply { + // Bind the LifecycleCameraController to the lifecycleOwner + bindToLifecycle(lifecycleOwner) + } + } + + val requestSinglePermission = + rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { permissionGranted -> + if (permissionGranted) { + context.toast("Single permission is granted.") + } else { + context.toast("Single permission is denied.") + } + } + + LaunchedEffect(Unit) { + requestSinglePermission.launch(Manifest.permission.CAMERA) + } + + ModalBottomSheet( + onDismissRequest = onDismissRequest, + sheetState = bottomSheetState + ) { + CustomBarcodeScannerScreenSkeleton( + message = null, + isFlashAvailable = cameraController.cameraInfo?.hasFlashUnit() == true, + isFlashOn = cameraController.imageCaptureFlashMode == FLASH_MODE_ON, + onFlashToggleClick = { + if (cameraController.imageCaptureFlashMode == FLASH_MODE_ON) { + cameraController.imageCaptureFlashMode = FLASH_MODE_OFF + } else { + cameraController.imageCaptureFlashMode = FLASH_MODE_ON + } + }, + cameraContent = { + CameraPreviewView( + onSuccess = { barcodes -> + barcodes.firstOrNull()?.let { + goBack() + currentOnSuccess(it) + } + } + ) + } + ) + } +} + +@PreviewLightDark +@Composable +private fun CustomBarcodeScannerScreenSkeletonPreview() { + AppTheme { + CustomBarcodeScannerScreenSkeleton( + message = null, + isFlashAvailable = true, + isFlashOn = true, + onFlashToggleClick = {}, + cameraContent = {} + ) + } +} + +@Suppress("ktlint:compose:modifier-missing-check") +@Composable +fun CustomBarcodeScannerScreenSkeleton( + message: Event?, + isFlashAvailable: Boolean, + isFlashOn: Boolean, + onFlashToggleClick: () -> Unit, + cameraContent: @Composable () -> Unit +) { + val snackbarHostState = remember { SnackbarHostState() } + val scope = rememberCoroutineScope() + + LaunchedEffect(message) { + message?.value?.let { message -> + scope.launch { + snackbarHostState.showSnackbar(message) + } + } + } + + CodeScannerView( + isFlashAvailable = isFlashAvailable, + isFlashOn = isFlashOn, + onFlashToggleClick = onFlashToggleClick + ) { + cameraContent() + } +} diff --git a/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/custombarcodescanner/CustomBarcodeScannerViewModel.kt b/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/custombarcodescanner/CustomBarcodeScannerViewModel.kt new file mode 100644 index 0000000..8c090e8 --- /dev/null +++ b/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/custombarcodescanner/CustomBarcodeScannerViewModel.kt @@ -0,0 +1,72 @@ +/* + * Copyright 2021 Md. Mahmudul Hasan Shohag + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ------------------------------------------------------------------------ + * + * Project: Why Not Compose! + * Developed by: @ImaginativeShohag + * + * Md. Mahmudul Hasan Shohag + * imaginativeshohag@gmail.com + * + * Source: https://github.com/ImaginativeShohag/Why-Not-Compose + */ + +package org.imaginativeworld.whynotcompose.ui.screens.tutorial.barcodescanner.custombarcodescanner + +import android.content.Context +import androidx.lifecycle.ViewModel +import com.google.mlkit.vision.barcode.common.Barcode +import com.google.mlkit.vision.codescanner.GmsBarcodeScannerOptions +import com.google.mlkit.vision.codescanner.GmsBarcodeScanning +import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import org.imaginativeworld.whynotcompose.base.models.Event + +@HiltViewModel +class CustomBarcodeScannerViewModel @Inject constructor( + @ApplicationContext private val appContext: Context +) : ViewModel() { + private val _scannedCode = MutableStateFlow(null) + val scannedCode = _scannedCode.asStateFlow() + + private val _message = MutableStateFlow?>(null) + val message = _message.asStateFlow() + + // ---------------------------------------------------------------- + + fun openGoogleBarcodeScanner() { + val options = GmsBarcodeScannerOptions.Builder() + .setBarcodeFormats(Barcode.FORMAT_ALL_FORMATS) + .enableAutoZoom() + .build() + + val scanner = GmsBarcodeScanning.getClient(appContext, options) + + scanner.startScan() + .addOnSuccessListener { barcode -> + _scannedCode.value = barcode + } + .addOnCanceledListener { + // Task canceled + } + .addOnFailureListener { e -> + _message.value = Event(e.message ?: "Unknown error! Try again.") + } + } +} diff --git a/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/BarcodeScannerScreen.kt b/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/index/BarcodeScannerScreen.kt similarity index 84% rename from app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/BarcodeScannerScreen.kt rename to app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/index/BarcodeScannerScreen.kt index d0bdb83..0658c1e 100644 --- a/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/BarcodeScannerScreen.kt +++ b/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/index/BarcodeScannerScreen.kt @@ -24,7 +24,7 @@ * Source: https://github.com/ImaginativeShohag/Why-Not-Compose */ -package org.imaginativeworld.whynotcompose.ui.screens.tutorial.barcodescanner +package org.imaginativeworld.whynotcompose.ui.screens.tutorial.barcodescanner.index import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -44,6 +44,8 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -51,12 +53,15 @@ import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.unit.dp import org.imaginativeworld.whynotcompose.common.compose.compositions.AppComponent import org.imaginativeworld.whynotcompose.common.compose.theme.AppTheme +import org.imaginativeworld.whynotcompose.ui.screens.tutorial.barcodescanner.custombarcodescanner.CustomBarcodeScannerSheet @Composable fun BarcodeScannerScreen( viewModel: BarcodeScannerViewModel, goBack: () -> Unit ) { + var showCustomCodeScanner by remember { mutableStateOf(false) } + val scannedCode = viewModel.scannedCode.collectAsState() BarcodeScannerScreenSkeleton( @@ -66,9 +71,24 @@ fun BarcodeScannerScreen( viewModel.openGoogleBarcodeScanner() }, onOpenCustomCodeScannerClick = { - // TODO: implement custom code scanner + showCustomCodeScanner = true } ) + + // ---------------------------------------------------------------- + // Sheets + // ---------------------------------------------------------------- + + if (showCustomCodeScanner) { + CustomBarcodeScannerSheet( + onDismissRequest = { + showCustomCodeScanner = false + }, + onSuccess = { barcode -> + viewModel.setScannedCode(barcode) + } + ) + } } @PreviewLightDark diff --git a/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/BarcodeScannerViewModel.kt b/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/index/BarcodeScannerViewModel.kt similarity index 95% rename from app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/BarcodeScannerViewModel.kt rename to app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/index/BarcodeScannerViewModel.kt index 6a72ad6..63c7829 100644 --- a/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/BarcodeScannerViewModel.kt +++ b/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/index/BarcodeScannerViewModel.kt @@ -24,7 +24,7 @@ * Source: https://github.com/ImaginativeShohag/Why-Not-Compose */ -package org.imaginativeworld.whynotcompose.ui.screens.tutorial.barcodescanner +package org.imaginativeworld.whynotcompose.ui.screens.tutorial.barcodescanner.index import android.content.Context import androidx.lifecycle.ViewModel @@ -65,4 +65,8 @@ class BarcodeScannerViewModel @Inject constructor( _scannedCode.value = e.message } } + + fun setScannedCode(barcode: Barcode) { + _scannedCode.value = barcode.rawValue + } } diff --git a/app/src/main/res/raw/qr_code_scanning.json b/app/src/main/res/raw/qr_code_scanning.json new file mode 100644 index 0000000..36b31f9 --- /dev/null +++ b/app/src/main/res/raw/qr_code_scanning.json @@ -0,0 +1 @@ +{"nm":"Main Scene","ddd":0,"h":638,"w":687,"meta":{"g":"@lottiefiles/creator 1.42.1"},"layers":[{"ty":4,"nm":"Shape Layer 6","sr":1,"st":0,"op":1978.00008056559,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[28,-20,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0.333,"y":0},"i":{"x":0.235,"y":1},"s":[345,165.5],"t":0},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[345,489],"t":33},{"o":{"x":0.297,"y":0},"i":{"x":0.224,"y":1},"s":[345,489],"t":39},{"o":{"x":0.167,"y":0},"i":{"x":0.235,"y":1},"s":[345,169],"t":70},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[345,489],"t":99},{"o":{"x":0.297,"y":0},"i":{"x":0.224,"y":1},"s":[345,489],"t":105},{"o":{"x":0.167,"y":0},"i":{"x":0.235,"y":1},"s":[345,169],"t":136},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[345,489],"t":163},{"o":{"x":0.297,"y":0},"i":{"x":0.224,"y":1},"s":[345,489],"t":169},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[345,169],"t":200},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[373.5,169,0],"t":200.000008146167},{"s":[345,169],"t":201}],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":100,"ix":11}},"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Shape 1","ix":1,"cix":2,"np":3,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"Path 1","ix":1,"d":1,"ks":{"a":0,"k":{"c":false,"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-156,-20],[212,-20]]},"ix":2}},{"ty":"st","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Stroke","nm":"Stroke 1","lc":2,"lj":1,"ml":4,"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":12,"ix":5},"c":{"a":0,"k":[0.5882,0.5882,0.5882],"ix":3}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[0,0],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]}],"ind":1},{"ty":4,"nm":"Shape Layer 4","sr":1,"st":0,"op":1978.00008056559,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[-194,-187.5,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[131,510],"ix":2},"r":{"a":0,"k":270,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":100,"ix":11}},"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Shape 1","ix":1,"cix":2,"np":3,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"Path 1","ix":1,"d":1,"ks":{"a":0,"k":{"c":false,"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-136,-244],[-252,-244],[-252,-131]]},"ix":2}},{"ty":"st","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Stroke","nm":"Stroke 1","lc":2,"lj":1,"ml":4,"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":20,"ix":5},"c":{"a":0,"k":[1,0,0],"ix":3}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[0,0],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]}],"ind":2},{"ty":4,"nm":"Shape Layer 3","sr":1,"st":0,"op":1978.00008056559,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[-194.00000000000006,-187.50000000000006],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[558.0000000000001,510],"ix":2},"r":{"a":0,"k":180,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":100,"ix":11}},"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Shape 1","ix":1,"cix":2,"np":3,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"Path 1","ix":1,"d":1,"ks":{"a":0,"k":{"c":false,"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-136,-244],[-252,-244],[-252,-131]]},"ix":2}},{"ty":"st","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Stroke","nm":"Stroke 1","lc":2,"lj":1,"ml":4,"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":20,"ix":5},"c":{"a":0,"k":[1,0,0],"ix":3}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[0,0],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]}],"ind":3},{"ty":4,"nm":"Shape Layer 5","sr":1,"st":0,"op":1978.00008056559,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[-194.00000000000006,-187.5000000000001],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[556.0000000000001,127.99999999999994],"ix":2},"r":{"a":0,"k":90,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":100,"ix":11}},"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Shape 1","ix":1,"cix":2,"np":3,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"Path 1","ix":1,"d":1,"ks":{"a":0,"k":{"c":false,"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-136,-244],[-252,-244],[-252,-131]]},"ix":2}},{"ty":"st","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Stroke","nm":"Stroke 1","lc":2,"lj":1,"ml":4,"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":20,"ix":5},"c":{"a":0,"k":[1,0,0],"ix":3}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[0,0],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]}],"ind":4},{"ty":4,"nm":"Shape Layer 2","sr":1,"st":0,"op":1978.00008056559,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[-194,-187.5,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[129,128],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":100,"ix":11}},"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Shape 1","ix":1,"cix":2,"np":3,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"Path 1","ix":1,"d":1,"ks":{"a":0,"k":{"c":false,"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-136,-244],[-252,-244],[-252,-131]]},"ix":2}},{"ty":"st","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Stroke","nm":"Stroke 1","lc":2,"lj":1,"ml":4,"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":20,"ix":5},"c":{"a":0,"k":[1,0,0],"ix":3}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[0,0],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]}],"ind":5}],"v":"5.7.0","fr":29.9700012207031,"op":202,"ip":0,"assets":[]} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5bae094..d2746ea 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -69,6 +69,8 @@ paging = "3.3.4" onesignal = "5.1.18" moshi = "1.15.2" swiperefreshlayout = "1.1.0" +mlkitBarcodeScanning = "17.3.0" +cameraX = "1.4.1" [bundles] androidx-compose-ui-test = ["androidx-compose-ui-test", "androidx-compose-ui-testManifest"] @@ -201,6 +203,22 @@ turbine = { group = "app.cash.turbine", name = "turbine", version.ref = "turbine moshi-kotlin = { group = "com.squareup.moshi", name = "moshi-kotlin", version.ref = "moshi" } moshi-kotlin-codegen = { group = "com.squareup.moshi", name = "moshi-kotlin-codegen", version.ref = "moshi" } +mlkit-barcode-scanning = { group = "com.google.mlkit", name = "barcode-scanning", version.ref = "mlkitBarcodeScanning" } + +# The following line is optional, as the core library is included indirectly by camera-camera2 +camerax-core = { group = "androidx.camera", name = "camera-core", version.ref = "cameraX" } +camerax-camera2 = { group = "androidx.camera", name = "camera-camera2", version.ref = "cameraX" } +# If you want to additionally use the CameraX Lifecycle library +camerax-lifecycle = { group = "androidx.camera", name = "camera-lifecycle", version.ref = "cameraX" } +# If you want to additionally use the CameraX VideoCapture library +camerax-video = { group = "androidx.camera", name = "camera-video", version.ref = "cameraX" } +# If you want to additionally use the CameraX View class +camerax-view = { group = "androidx.camera", name = "camera-view", version.ref = "cameraX" } +# If you want to additionally add CameraX ML Kit Vision Integration +camerax-mlkit-vision = { group = "androidx.camera", name = "camera-mlkit-vision", version.ref = "cameraX" } +# If you want to additionally use the CameraX Extensions library +camerax-extensions = { group = "androidx.camera", name = "camera-extensions", version.ref = "cameraX" } + # Dependencies of the included build-logic android-gradlePlugin = { group = "com.android.tools.build", name = "gradle", version.ref = "androidGradlePlugin" } android-tools-common = { group = "com.android.tools", name = "common", version.ref = "androidTools" } From 1bb10b37cdd2dde02a950b23c7a515458d5b1efa Mon Sep 17 00:00:00 2001 From: Mahmudul Hasan Shohag Date: Mon, 23 Dec 2024 02:50:38 +0600 Subject: [PATCH 3/9] feat(barcodescanner): add permission handling, refurnish qr code scanning, convert custom screen to sheet --- app/build.gradle.kts | 1 + .../custombarcodescanner/BarcodeAnalyser.kt | 51 ------ .../CustomBarcodeScannerSheet.kt | 168 ++++++++++++++---- .../components/BarcodeAnalyser.kt | 72 ++++++++ .../{ => components}/CameraPreviewView.kt | 100 ++--------- .../components/CodeScannerView.kt | 86 +++++++++ 6 files changed, 304 insertions(+), 174 deletions(-) delete mode 100644 app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/custombarcodescanner/BarcodeAnalyser.kt create mode 100644 app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/custombarcodescanner/components/BarcodeAnalyser.kt rename app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/custombarcodescanner/{ => components}/CameraPreviewView.kt (55%) create mode 100644 app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/custombarcodescanner/components/CodeScannerView.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 4bd91c6..1a8cda7 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -175,6 +175,7 @@ dependencies { implementation(libs.accompanist.systemuicontroller) implementation(libs.accompanist.flowlayout) implementation(libs.accompanist.swiperefresh) + implementation(libs.accompanist.permissions) // ---------------------------------------------------------------- diff --git a/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/custombarcodescanner/BarcodeAnalyser.kt b/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/custombarcodescanner/BarcodeAnalyser.kt deleted file mode 100644 index 16c1120..0000000 --- a/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/custombarcodescanner/BarcodeAnalyser.kt +++ /dev/null @@ -1,51 +0,0 @@ -package org.imaginativeworld.whynotcompose.ui.screens.tutorial.barcodescanner.custombarcodescanner - -import android.annotation.SuppressLint -import androidx.camera.core.ImageAnalysis -import androidx.camera.core.ImageProxy -import androidx.camera.view.LifecycleCameraController -import com.google.mlkit.vision.barcode.BarcodeScannerOptions -import com.google.mlkit.vision.barcode.BarcodeScanning -import com.google.mlkit.vision.barcode.ZoomSuggestionOptions -import com.google.mlkit.vision.barcode.common.Barcode -import com.google.mlkit.vision.common.InputImage -import timber.log.Timber - -@SuppressLint("UnsafeOptInUsageError") -class BarcodeAnalyser( - private val controller: LifecycleCameraController, - private val onBarcodeDetected: (barcodes: List) -> Unit -) : ImageAnalysis.Analyzer { - override fun analyze(image: ImageProxy) { - image.image?.let { imageToAnalyze -> - val options = BarcodeScannerOptions.Builder() - .setBarcodeFormats(Barcode.FORMAT_ALL_FORMATS) - .setZoomSuggestionOptions( - ZoomSuggestionOptions.Builder { zoomRatio -> - Timber.d("zoom suggestions ratio: $zoomRatio") - controller.setZoomRatio(zoomRatio) - true - }.build() - ) - .build() - - val barcodeScanner = BarcodeScanning.getClient(options) - val imageToProcess = InputImage.fromMediaImage(imageToAnalyze, image.imageInfo.rotationDegrees) - - barcodeScanner.process(imageToProcess) - .addOnSuccessListener { barcodes -> - if (barcodes.isNotEmpty()) { - onBarcodeDetected(barcodes) - } else { - Timber.d("No barcode Scanned") - } - } - .addOnFailureListener { exception -> - Timber.e(exception) - } - .addOnCompleteListener { - image.close() - } - } ?: image.close() - } -} diff --git a/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/custombarcodescanner/CustomBarcodeScannerSheet.kt b/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/custombarcodescanner/CustomBarcodeScannerSheet.kt index 5ad6cdc..eff2408 100644 --- a/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/custombarcodescanner/CustomBarcodeScannerSheet.kt +++ b/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/custombarcodescanner/CustomBarcodeScannerSheet.kt @@ -27,13 +27,24 @@ package org.imaginativeworld.whynotcompose.ui.screens.tutorial.barcodescanner.custombarcodescanner import android.Manifest -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.contract.ActivityResultContracts +import android.content.pm.PackageManager import androidx.camera.core.ImageCapture.FLASH_MODE_OFF import androidx.camera.core.ImageCapture.FLASH_MODE_ON import androidx.camera.view.LifecycleCameraController +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +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.padding +import androidx.compose.material3.BottomSheetDefaults +import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -41,18 +52,27 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.PreviewLightDark +import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.LocalLifecycleOwner +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.isGranted +import com.google.accompanist.permissions.rememberPermissionState +import com.google.accompanist.permissions.shouldShowRationale import com.google.mlkit.vision.barcode.common.Barcode import kotlinx.coroutines.launch -import org.imaginativeworld.whynotcompose.base.extensions.toast import org.imaginativeworld.whynotcompose.base.models.Event import org.imaginativeworld.whynotcompose.common.compose.theme.AppTheme +import org.imaginativeworld.whynotcompose.ui.screens.tutorial.barcodescanner.custombarcodescanner.components.CameraPreviewView +import org.imaginativeworld.whynotcompose.ui.screens.tutorial.barcodescanner.custombarcodescanner.components.CodeScannerView -// todo#1: add accompanist permission and add permission uis // todo#2: add no camera ui +@OptIn(ExperimentalPermissionsApi::class) @Composable fun CustomBarcodeScannerSheet( onDismissRequest: () -> Unit, @@ -65,6 +85,12 @@ fun CustomBarcodeScannerSheet( val bottomSheetState = rememberModalBottomSheetState( skipPartiallyExpanded = true ) + val cameraPermissionState = rememberPermissionState( + Manifest.permission.CAMERA + ) + val hasAnyCamera = remember { + context.packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY) == true + } val goBack: () -> Unit = { scope.launch { @@ -80,45 +106,60 @@ fun CustomBarcodeScannerSheet( } } - val requestSinglePermission = - rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { permissionGranted -> - if (permissionGranted) { - context.toast("Single permission is granted.") - } else { - context.toast("Single permission is denied.") - } - } - - LaunchedEffect(Unit) { - requestSinglePermission.launch(Manifest.permission.CAMERA) - } - ModalBottomSheet( onDismissRequest = onDismissRequest, sheetState = bottomSheetState ) { - CustomBarcodeScannerScreenSkeleton( - message = null, - isFlashAvailable = cameraController.cameraInfo?.hasFlashUnit() == true, - isFlashOn = cameraController.imageCaptureFlashMode == FLASH_MODE_ON, - onFlashToggleClick = { - if (cameraController.imageCaptureFlashMode == FLASH_MODE_ON) { - cameraController.imageCaptureFlashMode = FLASH_MODE_OFF - } else { - cameraController.imageCaptureFlashMode = FLASH_MODE_ON - } - }, - cameraContent = { - CameraPreviewView( - onSuccess = { barcodes -> - barcodes.firstOrNull()?.let { - goBack() - currentOnSuccess(it) - } + if (!hasAnyCamera) { + Box( + Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Text("Camera not available!") + } + } else if (cameraPermissionState.status.isGranted) { + CustomBarcodeScannerScreenSkeleton( + message = null, + isFlashAvailable = cameraController.cameraInfo?.hasFlashUnit() == true, + isFlashOn = cameraController.imageCaptureFlashMode == FLASH_MODE_ON, + onFlashToggleClick = { + if (cameraController.imageCaptureFlashMode == FLASH_MODE_ON) { + cameraController.imageCaptureFlashMode = FLASH_MODE_OFF + } else { + cameraController.imageCaptureFlashMode = FLASH_MODE_ON } - ) + }, + cameraContent = { + CameraPreviewView( + onSuccess = { barcodes -> + barcodes.firstOrNull()?.let { + goBack() + currentOnSuccess(it) + } + } + ) + } + ) + } else { + val textToShow = if (cameraPermissionState.status.shouldShowRationale) { + // If the user has denied the permission but the rationale can be shown, + // then gently explain why the app requires this permission + "The camera is needed to use the scanner. Please grant the permission." + } else { + // If it's the first time the user lands on this feature, or the user + // doesn't want to be asked again for this permission, explain that the + // permission is required + "Camera permission required for this feature to be available. " + + "Please grant the permission" } - ) + + RequestPermissionSection( + message = textToShow, + onRequestPermissionClick = { + cameraPermissionState.launchPermissionRequest() + } + ) + } } } @@ -131,7 +172,9 @@ private fun CustomBarcodeScannerScreenSkeletonPreview() { isFlashAvailable = true, isFlashOn = true, onFlashToggleClick = {}, - cameraContent = {} + cameraContent = {}, + modifier = Modifier + .background(BottomSheetDefaults.ContainerColor) ) } } @@ -143,7 +186,8 @@ fun CustomBarcodeScannerScreenSkeleton( isFlashAvailable: Boolean, isFlashOn: Boolean, onFlashToggleClick: () -> Unit, - cameraContent: @Composable () -> Unit + cameraContent: @Composable () -> Unit, + modifier: Modifier = Modifier ) { val snackbarHostState = remember { SnackbarHostState() } val scope = rememberCoroutineScope() @@ -157,6 +201,7 @@ fun CustomBarcodeScannerScreenSkeleton( } CodeScannerView( + modifier = modifier, isFlashAvailable = isFlashAvailable, isFlashOn = isFlashOn, onFlashToggleClick = onFlashToggleClick @@ -164,3 +209,48 @@ fun CustomBarcodeScannerScreenSkeleton( cameraContent() } } + +@PreviewLightDark +@Composable +private fun RequestPermissionSectionPreview() { + AppTheme { + RequestPermissionSection( + message = "The camera is needed to use the scanner. Please grant the permission.", + onRequestPermissionClick = {}, + modifier = Modifier + .fillMaxSize() + .background(BottomSheetDefaults.ContainerColor) + ) + } +} + +@Composable +private fun RequestPermissionSection( + message: String, + onRequestPermissionClick: () -> Unit, + modifier: Modifier = Modifier +) { + Box( + modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Column( + Modifier + .fillMaxWidth() + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + message, + color = MaterialTheme.colorScheme.onSurface, + textAlign = TextAlign.Center + ) + + Spacer(Modifier.height(16.dp)) + + Button(onClick = onRequestPermissionClick) { + Text("Request permission") + } + } + } +} diff --git a/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/custombarcodescanner/components/BarcodeAnalyser.kt b/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/custombarcodescanner/components/BarcodeAnalyser.kt new file mode 100644 index 0000000..b1ddb08 --- /dev/null +++ b/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/custombarcodescanner/components/BarcodeAnalyser.kt @@ -0,0 +1,72 @@ +package org.imaginativeworld.whynotcompose.ui.screens.tutorial.barcodescanner.custombarcodescanner.components + +import android.os.Handler +import android.os.Looper +import androidx.annotation.OptIn +import androidx.camera.core.ExperimentalGetImage +import androidx.camera.core.ImageAnalysis +import androidx.camera.core.ImageProxy +import androidx.camera.view.LifecycleCameraController +import com.google.mlkit.vision.barcode.BarcodeScanner +import com.google.mlkit.vision.barcode.BarcodeScannerOptions +import com.google.mlkit.vision.barcode.BarcodeScanning +import com.google.mlkit.vision.barcode.ZoomSuggestionOptions +import com.google.mlkit.vision.barcode.common.Barcode +import com.google.mlkit.vision.common.InputImage +import timber.log.Timber + +class BarcodeAnalyser( + private val controller: LifecycleCameraController, + private val onBarcodeDetected: (barcodes: List) -> Unit +) : ImageAnalysis.Analyzer { + + val barcodeScanner: BarcodeScanner + + init { + val options = BarcodeScannerOptions.Builder() + .setBarcodeFormats(Barcode.FORMAT_ALL_FORMATS) + .setZoomSuggestionOptions( + ZoomSuggestionOptions.Builder { zoomRatio -> + Handler(Looper.getMainLooper()).post { + Timber.d("Zoom suggestions ratio: $zoomRatio (Max: ${controller.cameraInfo?.zoomState?.value?.maxZoomRatio})") + controller.setZoomRatio(zoomRatio) + } + true + } + .setMaxSupportedZoomRatio(controller.cameraInfo?.zoomState?.value?.maxZoomRatio ?: 1f) + .build() + ) + .build() + + barcodeScanner = BarcodeScanning.getClient(options) + } + + @OptIn(ExperimentalGetImage::class) + override fun analyze(imageProxy: ImageProxy) { + val mediaImage = imageProxy.image + + if (mediaImage != null) { + val imageToProcess = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees) + + barcodeScanner.process(imageToProcess) + .addOnSuccessListener { barcodes -> + if (barcodes.isNotEmpty()) { + onBarcodeDetected(barcodes) + } else { + Timber.d("No barcode Scanned") + } + } + .addOnFailureListener { exception -> + Timber.d("Scan failure") + Timber.e(exception) + } + .addOnCompleteListener { + Timber.d("Scan task completed") + imageProxy.close() + } + } else { + Timber.d("Image proxy has no image") + imageProxy.close() + } + } +} diff --git a/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/custombarcodescanner/CameraPreviewView.kt b/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/custombarcodescanner/components/CameraPreviewView.kt similarity index 55% rename from app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/custombarcodescanner/CameraPreviewView.kt rename to app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/custombarcodescanner/components/CameraPreviewView.kt index 615888c..1c15317 100644 --- a/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/custombarcodescanner/CameraPreviewView.kt +++ b/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/custombarcodescanner/components/CameraPreviewView.kt @@ -1,4 +1,4 @@ -package org.imaginativeworld.whynotcompose.ui.screens.tutorial.barcodescanner.custombarcodescanner +package org.imaginativeworld.whynotcompose.ui.screens.tutorial.barcodescanner.custombarcodescanner.components import android.view.ViewGroup import androidx.camera.core.CameraSelector @@ -7,36 +7,18 @@ 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.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.FlashOff -import androidx.compose.material.icons.rounded.FlashOn -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue 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.tooling.preview.PreviewLightDark -import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import androidx.core.content.ContextCompat import androidx.lifecycle.compose.LocalLifecycleOwner -import com.airbnb.lottie.compose.LottieAnimation -import com.airbnb.lottie.compose.LottieCompositionSpec -import com.airbnb.lottie.compose.LottieConstants -import com.airbnb.lottie.compose.rememberLottieComposition import com.google.common.util.concurrent.ListenableFuture import com.google.mlkit.vision.barcode.common.Barcode +import org.imaginativeworld.whynotcompose.ui.screens.tutorial.barcodescanner.custombarcodescanner.components.BarcodeAnalyser import java.util.concurrent.ExecutorService import java.util.concurrent.Executors -import org.imaginativeworld.whynotcompose.R -import org.imaginativeworld.whynotcompose.common.compose.theme.AppTheme import timber.log.Timber @Composable @@ -53,12 +35,16 @@ fun CameraPreviewView( bindToLifecycle(lifecycleOwner) } } + var cameraProvider: ProcessCameraProvider? = remember { null } + + val stopCamera = { + cameraController.unbind() + cameraProvider?.unbindAll() + } AndroidView( factory = { androidViewContext -> val previewView = PreviewView(androidViewContext).apply { - setBackgroundColor(android.graphics.Color.GREEN) - this.scaleType = PreviewView.ScaleType.FILL_CENTER layoutParams = ViewGroup.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT @@ -74,16 +60,16 @@ fun CameraPreviewView( val cameraProviderFuture: ListenableFuture = ProcessCameraProvider.getInstance(context) cameraProviderFuture.addListener({ - val preview = Preview.Builder() - .build() - .also { - it.surfaceProvider = previewView.surfaceProvider - } + val preview = Preview.Builder().build().also { + it.surfaceProvider = previewView.surfaceProvider + } - val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get() + cameraProvider = cameraProviderFuture.get() val barcodeAnalyser = BarcodeAnalyser( controller = cameraController ) { barcodes -> + stopCamera() + onSuccess(barcodes) } val imageAnalysis: ImageAnalysis = ImageAnalysis.Builder() @@ -115,62 +101,8 @@ fun CameraPreviewView( }, modifier = modifier, onRelease = { - cameraController.unbind() + Timber.d("Released...") + stopCamera() } ) } - -@Composable -fun CodeScannerView( - isFlashAvailable: Boolean, - isFlashOn: Boolean, - onFlashToggleClick: () -> Unit, - modifier: Modifier = Modifier, - cameraContent: @Composable () -> Unit -) { - val composition by rememberLottieComposition( - LottieCompositionSpec.RawRes(R.raw.qr_code_scanning) - ) - - Box(modifier = modifier.fillMaxSize()) { - cameraContent() - - LottieAnimation( - modifier = Modifier - .size(250.dp) - .align(Alignment.Center), - composition = composition, - iterations = LottieConstants.IterateForever - ) - - if (isFlashAvailable) { - IconButton( - onClick = onFlashToggleClick, - modifier = Modifier.padding(16.dp) - ) { - Icon( - if (isFlashOn) { - Icons.Rounded.FlashOn - } else { - Icons.Rounded.FlashOff - }, - contentDescription = "Toggle Flash" - ) - } - } - } -} - -@PreviewLightDark -@Composable -private fun CameraPreviewViewPreview() { - AppTheme { - CodeScannerView( - isFlashAvailable = true, - isFlashOn = true, - onFlashToggleClick = {} - ) { - // camera preview view - } - } -} diff --git a/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/custombarcodescanner/components/CodeScannerView.kt b/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/custombarcodescanner/components/CodeScannerView.kt new file mode 100644 index 0000000..0941c33 --- /dev/null +++ b/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/custombarcodescanner/components/CodeScannerView.kt @@ -0,0 +1,86 @@ +package org.imaginativeworld.whynotcompose.ui.screens.tutorial.barcodescanner.custombarcodescanner.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.FlashOff +import androidx.compose.material.icons.rounded.FlashOn +import androidx.compose.material3.BottomSheetDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.PreviewLightDark +import androidx.compose.ui.unit.dp +import com.airbnb.lottie.compose.LottieAnimation +import com.airbnb.lottie.compose.LottieCompositionSpec +import com.airbnb.lottie.compose.LottieConstants +import com.airbnb.lottie.compose.rememberLottieComposition +import org.imaginativeworld.whynotcompose.R +import org.imaginativeworld.whynotcompose.common.compose.theme.AppTheme + +@Composable +fun CodeScannerView( + isFlashAvailable: Boolean, + isFlashOn: Boolean, + onFlashToggleClick: () -> Unit, + modifier: Modifier = Modifier, + cameraContent: @Composable () -> Unit +) { + val composition by rememberLottieComposition( + LottieCompositionSpec.RawRes(R.raw.qr_code_scanning) + ) + + Box(modifier = modifier.fillMaxSize()) { + cameraContent() + + LottieAnimation( + modifier = Modifier + .size(250.dp) + .align(Alignment.Center), + composition = composition, + iterations = LottieConstants.IterateForever + ) + + if (isFlashAvailable) { + IconButton( + onClick = onFlashToggleClick, + modifier = Modifier + .align(Alignment.BottomEnd) + .padding(16.dp) + ) { + Icon( + if (isFlashOn) { + Icons.Rounded.FlashOn + } else { + Icons.Rounded.FlashOff + }, + contentDescription = "Toggle Flash", + tint = MaterialTheme.colorScheme.onSurface + ) + } + } + } +} + +@PreviewLightDark +@Composable +private fun CameraPreviewViewPreview() { + AppTheme { + CodeScannerView( + modifier = Modifier + .background(BottomSheetDefaults.ContainerColor), + isFlashAvailable = true, + isFlashOn = true, + onFlashToggleClick = {} + ) { + // camera preview view + } + } +} From 11526444161a5785633650b658433f6fc9a6d297 Mon Sep 17 00:00:00 2001 From: Mahmudul Hasan Shohag Date: Mon, 23 Dec 2024 03:07:32 +0600 Subject: [PATCH 4/9] feat(barcode-scanner): Use `torchState` instead of `imageCaptureFlashMode` and pass `cameraController` to `CameraPreviewView` --- .../CustomBarcodeScannerSheet.kt | 19 ++++++++++--------- .../components/CameraPreviewView.kt | 13 +++++++------ 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/custombarcodescanner/CustomBarcodeScannerSheet.kt b/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/custombarcodescanner/CustomBarcodeScannerSheet.kt index eff2408..b74752a 100644 --- a/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/custombarcodescanner/CustomBarcodeScannerSheet.kt +++ b/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/custombarcodescanner/CustomBarcodeScannerSheet.kt @@ -28,8 +28,7 @@ package org.imaginativeworld.whynotcompose.ui.screens.tutorial.barcodescanner.cu import android.Manifest import android.content.pm.PackageManager -import androidx.camera.core.ImageCapture.FLASH_MODE_OFF -import androidx.camera.core.ImageCapture.FLASH_MODE_ON +import androidx.camera.core.TorchState import androidx.camera.view.LifecycleCameraController import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box @@ -49,6 +48,7 @@ import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberUpdatedState @@ -70,8 +70,6 @@ import org.imaginativeworld.whynotcompose.common.compose.theme.AppTheme import org.imaginativeworld.whynotcompose.ui.screens.tutorial.barcodescanner.custombarcodescanner.components.CameraPreviewView import org.imaginativeworld.whynotcompose.ui.screens.tutorial.barcodescanner.custombarcodescanner.components.CodeScannerView -// todo#2: add no camera ui - @OptIn(ExperimentalPermissionsApi::class) @Composable fun CustomBarcodeScannerSheet( @@ -106,6 +104,8 @@ fun CustomBarcodeScannerSheet( } } + val torchState = cameraController.torchState.observeAsState() + ModalBottomSheet( onDismissRequest = onDismissRequest, sheetState = bottomSheetState @@ -120,17 +120,18 @@ fun CustomBarcodeScannerSheet( } else if (cameraPermissionState.status.isGranted) { CustomBarcodeScannerScreenSkeleton( message = null, - isFlashAvailable = cameraController.cameraInfo?.hasFlashUnit() == true, - isFlashOn = cameraController.imageCaptureFlashMode == FLASH_MODE_ON, + isFlashAvailable = true, + isFlashOn = torchState.value == TorchState.ON, onFlashToggleClick = { - if (cameraController.imageCaptureFlashMode == FLASH_MODE_ON) { - cameraController.imageCaptureFlashMode = FLASH_MODE_OFF + if (torchState.value == TorchState.ON) { + cameraController.enableTorch(false) } else { - cameraController.imageCaptureFlashMode = FLASH_MODE_ON + cameraController.enableTorch(true) } }, cameraContent = { CameraPreviewView( + cameraController = cameraController, onSuccess = { barcodes -> barcodes.firstOrNull()?.let { goBack() diff --git a/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/custombarcodescanner/components/CameraPreviewView.kt b/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/custombarcodescanner/components/CameraPreviewView.kt index 1c15317..6de4b8c 100644 --- a/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/custombarcodescanner/components/CameraPreviewView.kt +++ b/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/custombarcodescanner/components/CameraPreviewView.kt @@ -23,18 +23,19 @@ import timber.log.Timber @Composable fun CameraPreviewView( + cameraController: LifecycleCameraController, onSuccess: (barcodes: List) -> Unit, modifier: Modifier = Modifier ) { val context = LocalContext.current val lifecycleOwner = LocalLifecycleOwner.current - val cameraController = remember { - LifecycleCameraController(context).apply { - // Bind the LifecycleCameraController to the lifecycleOwner - bindToLifecycle(lifecycleOwner) - } - } +// val cameraController = remember { +// LifecycleCameraController(context).apply { +// // Bind the LifecycleCameraController to the lifecycleOwner +// bindToLifecycle(lifecycleOwner) +// } +// } var cameraProvider: ProcessCameraProvider? = remember { null } val stopCamera = { From aa217cfc5744c34ad802d296a77773888a0e554b Mon Sep 17 00:00:00 2001 From: Mahmudul Hasan Shohag Date: Tue, 24 Dec 2024 02:18:27 +0600 Subject: [PATCH 5/9] refactor(barcode-scanner): apply spotless --- README.md | 3 +- .../whynotcompose/ui/screens/NavGraphMain.kt | 2 - .../components/BarcodeAnalyser.kt | 37 ++++++++++++++++--- .../components/CameraPreviewView.kt | 34 +++++++++++++---- .../components/CodeScannerView.kt | 26 +++++++++++++ .../index/BarcodeScannerScreen.kt | 2 +- .../BaselineProfilesViewModel.kt | 2 +- 7 files changed, 88 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 0fe10c3..06c44ad 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ Feel free to request features or suggestions for improvements. - Counter (Beginner) - Counter with ViewModel (Beginner) -- AnimatedVisibility (Beginner) +- `AnimatedVisibility` (Beginner) - Lottie (Beginner) - Select image and crop for upload (Intermediate) - Capture image and crop for upload (Intermediate) @@ -72,6 +72,7 @@ Feel free to request features or suggestions for improvements. - Navigation Data Pass (Intermediate) - Reactive Model (Beginner) - Baseline Profiles (Intermediate) +- [Barcode Scanner](https://developers.google.com/ml-kit/vision/barcode-scanning) ([Google code scanner](https://developers.google.com/ml-kit/vision/barcode-scanning/code-scanner) and [ML Kit Barcode](https://developers.google.com/ml-kit/vision/barcode-scanning/android)) (Intermediate) | ![Counter](images/counter.gif) | ![Animated Visibility](images/animated-visibility.gif) | ![Lottie](images/lottie.gif) | |:------------------------------------:|:------------------------------------------------------:|:----------------------------:| diff --git a/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/NavGraphMain.kt b/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/NavGraphMain.kt index c2fcf64..77541f6 100644 --- a/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/NavGraphMain.kt +++ b/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/NavGraphMain.kt @@ -108,8 +108,6 @@ import org.imaginativeworld.whynotcompose.ui.screens.composition.textfield.TextF import org.imaginativeworld.whynotcompose.ui.screens.home.index.HomeIndexScreen import org.imaginativeworld.whynotcompose.ui.screens.home.splash.SplashScreen import org.imaginativeworld.whynotcompose.ui.screens.tutorial.animatedvisibility.AnimatedVisibilityScreen -import org.imaginativeworld.whynotcompose.ui.screens.tutorial.barcodescanner.custombarcodescanner.CustomBarcodeScannerSheet -import org.imaginativeworld.whynotcompose.ui.screens.tutorial.barcodescanner.custombarcodescanner.CustomBarcodeScannerViewModel import org.imaginativeworld.whynotcompose.ui.screens.tutorial.barcodescanner.index.BarcodeScannerScreen import org.imaginativeworld.whynotcompose.ui.screens.tutorial.barcodescanner.index.BarcodeScannerViewModel import org.imaginativeworld.whynotcompose.ui.screens.tutorial.baselineprofiles.BaselineProfilesScreen diff --git a/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/custombarcodescanner/components/BarcodeAnalyser.kt b/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/custombarcodescanner/components/BarcodeAnalyser.kt index b1ddb08..a131a20 100644 --- a/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/custombarcodescanner/components/BarcodeAnalyser.kt +++ b/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/custombarcodescanner/components/BarcodeAnalyser.kt @@ -1,3 +1,29 @@ +/* + * Copyright 2024 Md. Mahmudul Hasan Shohag + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ------------------------------------------------------------------------ + * + * Project: Why Not Compose! + * Developed by: @ImaginativeShohag + * + * Md. Mahmudul Hasan Shohag + * imaginativeshohag@gmail.com + * + * Source: https://github.com/ImaginativeShohag/Why-Not-Compose + */ + package org.imaginativeworld.whynotcompose.ui.screens.tutorial.barcodescanner.custombarcodescanner.components import android.os.Handler @@ -19,8 +45,8 @@ class BarcodeAnalyser( private val controller: LifecycleCameraController, private val onBarcodeDetected: (barcodes: List) -> Unit ) : ImageAnalysis.Analyzer { - - val barcodeScanner: BarcodeScanner + private val barcodeScanner: BarcodeScanner + private val maxZoomRatio: Float = controller.cameraInfo?.zoomState?.value?.maxZoomRatio ?: 1f init { val options = BarcodeScannerOptions.Builder() @@ -28,12 +54,13 @@ class BarcodeAnalyser( .setZoomSuggestionOptions( ZoomSuggestionOptions.Builder { zoomRatio -> Handler(Looper.getMainLooper()).post { - Timber.d("Zoom suggestions ratio: $zoomRatio (Max: ${controller.cameraInfo?.zoomState?.value?.maxZoomRatio})") + Timber.d("Zoom suggestions ratio: $zoomRatio (Max: $maxZoomRatio)") + controller.setZoomRatio(zoomRatio) } true } - .setMaxSupportedZoomRatio(controller.cameraInfo?.zoomState?.value?.maxZoomRatio ?: 1f) + .setMaxSupportedZoomRatio(maxZoomRatio) .build() ) .build() @@ -53,7 +80,7 @@ class BarcodeAnalyser( if (barcodes.isNotEmpty()) { onBarcodeDetected(barcodes) } else { - Timber.d("No barcode Scanned") + Timber.d("No barcode found") } } .addOnFailureListener { exception -> diff --git a/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/custombarcodescanner/components/CameraPreviewView.kt b/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/custombarcodescanner/components/CameraPreviewView.kt index 6de4b8c..bc2d2b0 100644 --- a/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/custombarcodescanner/components/CameraPreviewView.kt +++ b/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/custombarcodescanner/components/CameraPreviewView.kt @@ -1,3 +1,29 @@ +/* + * Copyright 2024 Md. Mahmudul Hasan Shohag + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ------------------------------------------------------------------------ + * + * Project: Why Not Compose! + * Developed by: @ImaginativeShohag + * + * Md. Mahmudul Hasan Shohag + * imaginativeshohag@gmail.com + * + * Source: https://github.com/ImaginativeShohag/Why-Not-Compose + */ + package org.imaginativeworld.whynotcompose.ui.screens.tutorial.barcodescanner.custombarcodescanner.components import android.view.ViewGroup @@ -16,7 +42,6 @@ import androidx.core.content.ContextCompat import androidx.lifecycle.compose.LocalLifecycleOwner import com.google.common.util.concurrent.ListenableFuture import com.google.mlkit.vision.barcode.common.Barcode -import org.imaginativeworld.whynotcompose.ui.screens.tutorial.barcodescanner.custombarcodescanner.components.BarcodeAnalyser import java.util.concurrent.ExecutorService import java.util.concurrent.Executors import timber.log.Timber @@ -30,12 +55,6 @@ fun CameraPreviewView( val context = LocalContext.current val lifecycleOwner = LocalLifecycleOwner.current -// val cameraController = remember { -// LifecycleCameraController(context).apply { -// // Bind the LifecycleCameraController to the lifecycleOwner -// bindToLifecycle(lifecycleOwner) -// } -// } var cameraProvider: ProcessCameraProvider? = remember { null } val stopCamera = { @@ -50,7 +69,6 @@ fun CameraPreviewView( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT ) - implementationMode = PreviewView.ImplementationMode.COMPATIBLE controller = cameraController } diff --git a/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/custombarcodescanner/components/CodeScannerView.kt b/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/custombarcodescanner/components/CodeScannerView.kt index 0941c33..1bcf997 100644 --- a/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/custombarcodescanner/components/CodeScannerView.kt +++ b/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/custombarcodescanner/components/CodeScannerView.kt @@ -1,3 +1,29 @@ +/* + * Copyright 2024 Md. Mahmudul Hasan Shohag + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * ------------------------------------------------------------------------ + * + * Project: Why Not Compose! + * Developed by: @ImaginativeShohag + * + * Md. Mahmudul Hasan Shohag + * imaginativeshohag@gmail.com + * + * Source: https://github.com/ImaginativeShohag/Why-Not-Compose + */ + package org.imaginativeworld.whynotcompose.ui.screens.tutorial.barcodescanner.custombarcodescanner.components import androidx.compose.foundation.background diff --git a/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/index/BarcodeScannerScreen.kt b/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/index/BarcodeScannerScreen.kt index 0658c1e..0f348a0 100644 --- a/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/index/BarcodeScannerScreen.kt +++ b/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/index/BarcodeScannerScreen.kt @@ -114,7 +114,7 @@ fun BarcodeScannerScreenSkeleton( .statusBarsPadding(), topBar = { AppComponent.Header( - "BarcodeScanner", + "Barcode Scanner", goBack = goBack ) } diff --git a/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/baselineprofiles/BaselineProfilesViewModel.kt b/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/baselineprofiles/BaselineProfilesViewModel.kt index d74f5bc..1f901d9 100644 --- a/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/baselineprofiles/BaselineProfilesViewModel.kt +++ b/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/baselineprofiles/BaselineProfilesViewModel.kt @@ -1,7 +1,7 @@ /* * Copyright 2021 Md. Mahmudul Hasan Shohag * - * Licensed under the Apache License, Version 2.0 (the "License"; + * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * From e656704f971bb415cf902e444b95dc863034abf2 Mon Sep 17 00:00:00 2001 From: Mahmudul Hasan Shohag Date: Tue, 24 Dec 2024 03:01:59 +0600 Subject: [PATCH 6/9] refactor(barcode-scanner): update result title text --- .../tutorial/barcodescanner/index/BarcodeScannerScreen.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/index/BarcodeScannerScreen.kt b/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/index/BarcodeScannerScreen.kt index 0f348a0..9e224b5 100644 --- a/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/index/BarcodeScannerScreen.kt +++ b/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/index/BarcodeScannerScreen.kt @@ -129,12 +129,12 @@ fun BarcodeScannerScreenSkeleton( verticalArrangement = Arrangement.Center ) { Text( - "Scanned Code", + "Scanned Data", style = MaterialTheme.typography.titleMedium ) if (scannedCode.isNullOrBlank()) { - Text("No Result") + Text("No Data") } else { Text(scannedCode) } From e8bff800b815c221d8e7b51366b64467f9bdba67 Mon Sep 17 00:00:00 2001 From: Mahmudul Hasan Shohag Date: Wed, 25 Dec 2024 14:39:50 +0600 Subject: [PATCH 7/9] refactor: update codeql action --- .github/workflows/codeql.yml | 33 +++++++++++---------------------- 1 file changed, 11 insertions(+), 22 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index b9d8464..727dabf 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -1,14 +1,3 @@ -# For most projects, this workflow file will not need changing; you simply need -# to commit it to your repository. -# -# You may wish to alter this file to override the set of languages analyzed, -# or to provide custom queries or build logic. -# -# ******** NOTE ******** -# We have attempted to detect the languages in your repository. Please check -# the `language` matrix defined below to confirm you have the correct set of -# supported CodeQL languages. -# name: "CodeQL" on: @@ -61,12 +50,12 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 - # - name: Enable KVM group perms - # run: | - # echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules - # sudo udevadm control --reload-rules - # sudo udevadm trigger --name-match=kvm - # ls /dev/kvm + - name: Enable KVM group perms + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + ls /dev/kvm - name: Initialize local properties env: @@ -115,11 +104,11 @@ jobs: - name: Run tests run: ./gradlew test - # - name: Setup GMD - # run: ./gradlew :benchmarks:pixel6Api35Setup - # --info - # -Pandroid.experimental.testOptions.managedDevices.emulator.showKernelLogging=true - # -Pandroid.testoptions.manageddevices.emulator.gpu="swiftshader_indirect" + - name: Setup GMD + run: ./gradlew :benchmarks:pixel6Api35Setup + --info + -Pandroid.experimental.testOptions.managedDevices.emulator.showKernelLogging=true + -Pandroid.testoptions.manageddevices.emulator.gpu="swiftshader_indirect" - name: Build all build type and flavor permutations (Ignore baseline profile generation) run: ./gradlew assemble From c570d57d20acc581ede398f1bc84fd886a9e86de Mon Sep 17 00:00:00 2001 From: Mahmudul Hasan Shohag Date: Fri, 27 Dec 2024 01:03:11 +0600 Subject: [PATCH 8/9] build: update dependencies --- README.md | 1 + app/build.gradle.kts | 3 --- base/build.gradle.kts | 5 ----- cms/build.gradle.kts | 5 ----- common-ui-compose/build.gradle.kts | 5 ----- exoplayer/build.gradle.kts | 5 ----- gradle/libs.versions.toml | 25 +++++++++++-------------- popbackstack/build.gradle.kts | 5 ----- tictactoe/build.gradle.kts | 5 ----- 9 files changed, 12 insertions(+), 47 deletions(-) diff --git a/README.md b/README.md index 06c44ad..e3581f3 100644 --- a/README.md +++ b/README.md @@ -106,6 +106,7 @@ Feel free to request features or suggestions for improvements. - [ ] Update all `LaunchedEffect` with lambda issue using `rememberUpdatedState`: https://developer.android.com/develop/ui/compose/side-effects#rememberupdatedstate - [ ] Add example for AppColorLocal from Jaber vai - [x] Update to coil 3 +- [ ] Update ktlint # Note diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 1a8cda7..3e4f403 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -172,9 +172,6 @@ dependencies { implementation(libs.androidx.paging.compose) // Accompanist - implementation(libs.accompanist.systemuicontroller) - implementation(libs.accompanist.flowlayout) - implementation(libs.accompanist.swiperefresh) implementation(libs.accompanist.permissions) // ---------------------------------------------------------------- diff --git a/base/build.gradle.kts b/base/build.gradle.kts index bd22fde..9af8d88 100644 --- a/base/build.gradle.kts +++ b/base/build.gradle.kts @@ -94,11 +94,6 @@ dependencies { // Paging implementation(libs.androidx.paging.compose) - // Accompanist - implementation(libs.accompanist.systemuicontroller) - implementation(libs.accompanist.flowlayout) - implementation(libs.accompanist.swiperefresh) - // ---------------------------------------------------------------- // Retrofit diff --git a/cms/build.gradle.kts b/cms/build.gradle.kts index 8daa7bc..8a3ecaf 100644 --- a/cms/build.gradle.kts +++ b/cms/build.gradle.kts @@ -120,11 +120,6 @@ dependencies { // Paging implementation(libs.androidx.paging.compose) - // Accompanist - implementation(libs.accompanist.systemuicontroller) - implementation(libs.accompanist.flowlayout) - implementation(libs.accompanist.swiperefresh) - // ---------------------------------------------------------------- // Timber diff --git a/common-ui-compose/build.gradle.kts b/common-ui-compose/build.gradle.kts index a6ccff8..f4bb41d 100644 --- a/common-ui-compose/build.gradle.kts +++ b/common-ui-compose/build.gradle.kts @@ -113,11 +113,6 @@ dependencies { // Paging implementation(libs.androidx.paging.compose) - // Accompanist - implementation(libs.accompanist.systemuicontroller) - implementation(libs.accompanist.flowlayout) - implementation(libs.accompanist.swiperefresh) - // ---------------------------------------------------------------- // Timber diff --git a/exoplayer/build.gradle.kts b/exoplayer/build.gradle.kts index 6575ecf..3a5519f 100644 --- a/exoplayer/build.gradle.kts +++ b/exoplayer/build.gradle.kts @@ -112,11 +112,6 @@ dependencies { // Paging implementation(libs.androidx.paging.compose) - // Accompanist - implementation(libs.accompanist.systemuicontroller) - implementation(libs.accompanist.flowlayout) - implementation(libs.accompanist.swiperefresh) - // ---------------------------------------------------------------- // Timber diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d2746ea..81d1ce0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -accompanist = "0.36.0" +accompanist = "0.37.0" androidDesugarJdkLibs = "2.0.4" # AGP and tools should be updated together androidGradlePlugin = "8.7.3" @@ -7,17 +7,17 @@ androidTools = "31.7.2" androidxActivity = "1.9.3" androidxAppCompat = "1.7.0" androidxBrowser = "1.8.0" -androidxComposeBom = "2024.11.00" +androidxComposeBom = "2024.12.01" androidxCore = "1.15.0" androidxCoreSplashscreen = "1.0.1" androidxDataStore = "1.1.1" androidxEspresso = "3.6.1" androidxHiltNavigationCompose = "1.2.0" androidxLifecycle = "2.8.7" -androidxMacroBenchmark = "1.3.1" +androidxMacroBenchmark = "1.3.3" androidxMetrics = "1.0.0-beta01" -androidxNavigation = "2.8.4" -androidxProfileinstaller = "1.3.1" +androidxNavigation = "2.8.5" +androidxProfileinstaller = "1.4.1" androidxTestCore = "1.6.1" androidxTestExt = "1.2.1" androidxTestRules = "1.6.1" @@ -29,16 +29,16 @@ androidxWork = "2.9.0" coil = "3.0.4" dependencyGuard = "0.5.0" firebaseBom = "33.7.0" -firebaseCrashlyticsPlugin = "2.9.9" +firebaseCrashlyticsPlugin = "3.0.2" firebasePerfPlugin = "1.4.2" gmsPlugin = "4.4.2" googleOss = "17.1.0" googleOssPlugin = "0.10.6" -hilt = "2.53.1" +hilt = "2.54" hiltExt = "1.2.0" junit = "4.13.2" kotlin = "2.0.21" -kotlinxCoroutines = "1.9.0" +kotlinxCoroutines = "1.10.1" kotlinxDatetime = "0.6.1" kotlinxSerializationJson = "1.7.3" ksp = "2.0.21-1.0.28" @@ -58,15 +58,15 @@ timber = "5.0.1" oopsNoInternet = "2.0.0" spotless = "6.25.0" ucrop = "2.2.10" -lottie = "6.6.1" +lottie = "6.6.2" exoplayer = "2.19.1" googlePlayServiceMaps = "19.0.0" googlePlayServiceCodeScanner = "16.1.0" googleMaps = "5.1.1" googleMapsCompose = "6.4.0" androidxConstraintlayoutCompose = "1.1.0" -paging = "3.3.4" -onesignal = "5.1.18" +paging = "3.3.5" +onesignal = "5.1.26" moshi = "1.15.2" swiperefreshlayout = "1.1.0" mlkitBarcodeScanning = "17.3.0" @@ -90,9 +90,6 @@ google-maps-compose = { group = "com.google.maps.android", name = "maps-compose" google-playservice-code-scanner = { group = "com.google.android.gms", name = "play-services-code-scanner", version.ref = "googlePlayServiceCodeScanner" } accompanist-permissions = { group = "com.google.accompanist", name = "accompanist-permissions", version.ref = "accompanist" } -accompanist-systemuicontroller = { group = "com.google.accompanist", name = "accompanist-systemuicontroller", version.ref = "accompanist" } -accompanist-flowlayout = { group = "com.google.accompanist", name = "accompanist-flowlayout", version.ref = "accompanist" } -accompanist-swiperefresh = { group = "com.google.accompanist", name = "accompanist-swiperefresh", version.ref = "accompanist" } onesignal = { group = "com.onesignal", name = "OneSignal", version.ref = "onesignal" } diff --git a/popbackstack/build.gradle.kts b/popbackstack/build.gradle.kts index b89fb85..d70c6c2 100644 --- a/popbackstack/build.gradle.kts +++ b/popbackstack/build.gradle.kts @@ -123,11 +123,6 @@ dependencies { // Paging implementation(libs.androidx.paging.compose) - // Accompanist - implementation(libs.accompanist.systemuicontroller) - implementation(libs.accompanist.flowlayout) - implementation(libs.accompanist.swiperefresh) - // ---------------------------------------------------------------- // Timber diff --git a/tictactoe/build.gradle.kts b/tictactoe/build.gradle.kts index 27c3c52..4b8e599 100644 --- a/tictactoe/build.gradle.kts +++ b/tictactoe/build.gradle.kts @@ -113,11 +113,6 @@ dependencies { // Paging implementation(libs.androidx.paging.compose) - // Accompanist - implementation(libs.accompanist.systemuicontroller) - implementation(libs.accompanist.flowlayout) - implementation(libs.accompanist.swiperefresh) - // ---------------------------------------------------------------- // Timber From c4ee87b3f9df150eb168998f41a58a8bf315e039 Mon Sep 17 00:00:00 2001 From: Mahmudul Hasan Shohag Date: Fri, 27 Dec 2024 01:33:15 +0600 Subject: [PATCH 9/9] refactor: remove unnecessary code --- .../CustomBarcodeScannerSheet.kt | 4 +- .../CustomBarcodeScannerViewModel.kt | 72 ------------------- .../components/BarcodeAnalyser.kt | 4 +- .../components/CameraPreviewView.kt | 18 ++--- app/src/main/res/raw/qr_code_scanning.json | 2 +- 5 files changed, 15 insertions(+), 85 deletions(-) delete mode 100644 app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/custombarcodescanner/CustomBarcodeScannerViewModel.kt diff --git a/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/custombarcodescanner/CustomBarcodeScannerSheet.kt b/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/custombarcodescanner/CustomBarcodeScannerSheet.kt index b74752a..4bac042 100644 --- a/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/custombarcodescanner/CustomBarcodeScannerSheet.kt +++ b/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/custombarcodescanner/CustomBarcodeScannerSheet.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021 Md. Mahmudul Hasan Shohag + * Copyright 2024 Md. Mahmudul Hasan Shohag * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -132,7 +132,7 @@ fun CustomBarcodeScannerSheet( cameraContent = { CameraPreviewView( cameraController = cameraController, - onSuccess = { barcodes -> + onBarcodeDetect = { barcodes -> barcodes.firstOrNull()?.let { goBack() currentOnSuccess(it) diff --git a/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/custombarcodescanner/CustomBarcodeScannerViewModel.kt b/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/custombarcodescanner/CustomBarcodeScannerViewModel.kt deleted file mode 100644 index 8c090e8..0000000 --- a/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/custombarcodescanner/CustomBarcodeScannerViewModel.kt +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright 2021 Md. Mahmudul Hasan Shohag - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * ------------------------------------------------------------------------ - * - * Project: Why Not Compose! - * Developed by: @ImaginativeShohag - * - * Md. Mahmudul Hasan Shohag - * imaginativeshohag@gmail.com - * - * Source: https://github.com/ImaginativeShohag/Why-Not-Compose - */ - -package org.imaginativeworld.whynotcompose.ui.screens.tutorial.barcodescanner.custombarcodescanner - -import android.content.Context -import androidx.lifecycle.ViewModel -import com.google.mlkit.vision.barcode.common.Barcode -import com.google.mlkit.vision.codescanner.GmsBarcodeScannerOptions -import com.google.mlkit.vision.codescanner.GmsBarcodeScanning -import dagger.hilt.android.lifecycle.HiltViewModel -import dagger.hilt.android.qualifiers.ApplicationContext -import javax.inject.Inject -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import org.imaginativeworld.whynotcompose.base.models.Event - -@HiltViewModel -class CustomBarcodeScannerViewModel @Inject constructor( - @ApplicationContext private val appContext: Context -) : ViewModel() { - private val _scannedCode = MutableStateFlow(null) - val scannedCode = _scannedCode.asStateFlow() - - private val _message = MutableStateFlow?>(null) - val message = _message.asStateFlow() - - // ---------------------------------------------------------------- - - fun openGoogleBarcodeScanner() { - val options = GmsBarcodeScannerOptions.Builder() - .setBarcodeFormats(Barcode.FORMAT_ALL_FORMATS) - .enableAutoZoom() - .build() - - val scanner = GmsBarcodeScanning.getClient(appContext, options) - - scanner.startScan() - .addOnSuccessListener { barcode -> - _scannedCode.value = barcode - } - .addOnCanceledListener { - // Task canceled - } - .addOnFailureListener { e -> - _message.value = Event(e.message ?: "Unknown error! Try again.") - } - } -} diff --git a/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/custombarcodescanner/components/BarcodeAnalyser.kt b/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/custombarcodescanner/components/BarcodeAnalyser.kt index a131a20..9704ee4 100644 --- a/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/custombarcodescanner/components/BarcodeAnalyser.kt +++ b/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/custombarcodescanner/components/BarcodeAnalyser.kt @@ -43,7 +43,7 @@ import timber.log.Timber class BarcodeAnalyser( private val controller: LifecycleCameraController, - private val onBarcodeDetected: (barcodes: List) -> Unit + private val onBarcodeDetect: (barcodes: List) -> Unit ) : ImageAnalysis.Analyzer { private val barcodeScanner: BarcodeScanner private val maxZoomRatio: Float = controller.cameraInfo?.zoomState?.value?.maxZoomRatio ?: 1f @@ -78,7 +78,7 @@ class BarcodeAnalyser( barcodeScanner.process(imageToProcess) .addOnSuccessListener { barcodes -> if (barcodes.isNotEmpty()) { - onBarcodeDetected(barcodes) + onBarcodeDetect(barcodes) } else { Timber.d("No barcode found") } diff --git a/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/custombarcodescanner/components/CameraPreviewView.kt b/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/custombarcodescanner/components/CameraPreviewView.kt index bc2d2b0..77c493c 100644 --- a/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/custombarcodescanner/components/CameraPreviewView.kt +++ b/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/barcodescanner/custombarcodescanner/components/CameraPreviewView.kt @@ -49,7 +49,7 @@ import timber.log.Timber @Composable fun CameraPreviewView( cameraController: LifecycleCameraController, - onSuccess: (barcodes: List) -> Unit, + onBarcodeDetect: (barcodes: List) -> Unit, modifier: Modifier = Modifier ) { val context = LocalContext.current @@ -84,14 +84,16 @@ fun CameraPreviewView( } cameraProvider = cameraProviderFuture.get() + val barcodeAnalyser = BarcodeAnalyser( - controller = cameraController - ) { barcodes -> - stopCamera() + controller = cameraController, + onBarcodeDetect = { barcodes -> + stopCamera() - onSuccess(barcodes) - } - val imageAnalysis: ImageAnalysis = ImageAnalysis.Builder() + onBarcodeDetect(barcodes) + } + ) + val imageAnalysis = ImageAnalysis.Builder() .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) .build() .also { @@ -120,7 +122,7 @@ fun CameraPreviewView( }, modifier = modifier, onRelease = { - Timber.d("Released...") + Timber.d("Releasing PreviewView.") stopCamera() } ) diff --git a/app/src/main/res/raw/qr_code_scanning.json b/app/src/main/res/raw/qr_code_scanning.json index 36b31f9..52ad2d3 100644 --- a/app/src/main/res/raw/qr_code_scanning.json +++ b/app/src/main/res/raw/qr_code_scanning.json @@ -1 +1 @@ -{"nm":"Main Scene","ddd":0,"h":638,"w":687,"meta":{"g":"@lottiefiles/creator 1.42.1"},"layers":[{"ty":4,"nm":"Shape Layer 6","sr":1,"st":0,"op":1978.00008056559,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[28,-20,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0.333,"y":0},"i":{"x":0.235,"y":1},"s":[345,165.5],"t":0},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[345,489],"t":33},{"o":{"x":0.297,"y":0},"i":{"x":0.224,"y":1},"s":[345,489],"t":39},{"o":{"x":0.167,"y":0},"i":{"x":0.235,"y":1},"s":[345,169],"t":70},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[345,489],"t":99},{"o":{"x":0.297,"y":0},"i":{"x":0.224,"y":1},"s":[345,489],"t":105},{"o":{"x":0.167,"y":0},"i":{"x":0.235,"y":1},"s":[345,169],"t":136},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[345,489],"t":163},{"o":{"x":0.297,"y":0},"i":{"x":0.224,"y":1},"s":[345,489],"t":169},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[345,169],"t":200},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[373.5,169,0],"t":200.000008146167},{"s":[345,169],"t":201}],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":100,"ix":11}},"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Shape 1","ix":1,"cix":2,"np":3,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"Path 1","ix":1,"d":1,"ks":{"a":0,"k":{"c":false,"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-156,-20],[212,-20]]},"ix":2}},{"ty":"st","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Stroke","nm":"Stroke 1","lc":2,"lj":1,"ml":4,"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":12,"ix":5},"c":{"a":0,"k":[0.5882,0.5882,0.5882],"ix":3}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[0,0],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]}],"ind":1},{"ty":4,"nm":"Shape Layer 4","sr":1,"st":0,"op":1978.00008056559,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[-194,-187.5,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[131,510],"ix":2},"r":{"a":0,"k":270,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":100,"ix":11}},"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Shape 1","ix":1,"cix":2,"np":3,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"Path 1","ix":1,"d":1,"ks":{"a":0,"k":{"c":false,"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-136,-244],[-252,-244],[-252,-131]]},"ix":2}},{"ty":"st","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Stroke","nm":"Stroke 1","lc":2,"lj":1,"ml":4,"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":20,"ix":5},"c":{"a":0,"k":[1,0,0],"ix":3}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[0,0],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]}],"ind":2},{"ty":4,"nm":"Shape Layer 3","sr":1,"st":0,"op":1978.00008056559,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[-194.00000000000006,-187.50000000000006],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[558.0000000000001,510],"ix":2},"r":{"a":0,"k":180,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":100,"ix":11}},"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Shape 1","ix":1,"cix":2,"np":3,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"Path 1","ix":1,"d":1,"ks":{"a":0,"k":{"c":false,"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-136,-244],[-252,-244],[-252,-131]]},"ix":2}},{"ty":"st","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Stroke","nm":"Stroke 1","lc":2,"lj":1,"ml":4,"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":20,"ix":5},"c":{"a":0,"k":[1,0,0],"ix":3}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[0,0],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]}],"ind":3},{"ty":4,"nm":"Shape Layer 5","sr":1,"st":0,"op":1978.00008056559,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[-194.00000000000006,-187.5000000000001],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[556.0000000000001,127.99999999999994],"ix":2},"r":{"a":0,"k":90,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":100,"ix":11}},"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Shape 1","ix":1,"cix":2,"np":3,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"Path 1","ix":1,"d":1,"ks":{"a":0,"k":{"c":false,"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-136,-244],[-252,-244],[-252,-131]]},"ix":2}},{"ty":"st","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Stroke","nm":"Stroke 1","lc":2,"lj":1,"ml":4,"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":20,"ix":5},"c":{"a":0,"k":[1,0,0],"ix":3}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[0,0],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]}],"ind":4},{"ty":4,"nm":"Shape Layer 2","sr":1,"st":0,"op":1978.00008056559,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[-194,-187.5,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[129,128],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":100,"ix":11}},"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Shape 1","ix":1,"cix":2,"np":3,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"Path 1","ix":1,"d":1,"ks":{"a":0,"k":{"c":false,"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-136,-244],[-252,-244],[-252,-131]]},"ix":2}},{"ty":"st","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Stroke","nm":"Stroke 1","lc":2,"lj":1,"ml":4,"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":20,"ix":5},"c":{"a":0,"k":[1,0,0],"ix":3}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[0,0],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]}],"ind":5}],"v":"5.7.0","fr":29.9700012207031,"op":202,"ip":0,"assets":[]} \ No newline at end of file +{"nm":"Comp 1","ddd":0,"h":700,"w":700,"meta":{"g":"@lottiefiles/toolkit-js 0.33.2"},"layers":[{"ty":4,"nm":"Shape Layer 1","sr":1,"st":0,"op":600.000024438501,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[1,-260,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":1,"y":0},"i":{"x":0.667,"y":1},"s":[351,90,0],"t":0,"ti":[0,0,0],"to":[0,86.833,0]},{"o":{"x":0.167,"y":0},"i":{"x":0.667,"y":1},"s":[351,611,0],"t":26.329,"ti":[0,86.833,0],"to":[0,0,0]},{"s":[351,90,0],"t":52.0000021180034}],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":100,"ix":11}},"ef":[],"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Shape 1","ix":1,"cix":2,"np":3,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"Path 1","ix":1,"d":1,"ks":{"a":0,"k":{"c":false,"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-266,-260],[268,-260]]},"ix":2}},{"ty":"st","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Stroke","nm":"Stroke 1","lc":2,"lj":1,"ml":4,"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":13,"ix":5},"c":{"a":0,"k":[1,0.2667,0.2667],"ix":3}},{"ty":"fl","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Fill","nm":"Fill 1","c":{"a":0,"k":[0.3294,0.3294,0.3294],"ix":4},"r":1,"o":{"a":0,"k":0,"ix":5}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[0,0],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]}],"ind":1},{"ty":0,"nm":"Pre-comp 1","sr":1,"st":0,"op":600.000024438501,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[350,350,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[350,350,0],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":100,"ix":11}},"ef":[],"w":700,"h":700,"refId":"comp_0","ind":2}],"v":"5.5.5","fr":29.9700012207031,"op":53.0000021587343,"ip":0,"assets":[{"nm":"","id":"comp_0","layers":[{"ty":4,"nm":"Shape Layer 4","sr":1,"st":0,"op":600.000024438501,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[-100,-100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[350,350,0],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":100,"ix":11}},"ef":[],"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Rectangle 1","ix":1,"cix":2,"np":3,"it":[{"ty":"rc","bm":0,"hd":false,"mn":"ADBE Vector Shape - Rect","nm":"Rectangle Path 1","d":1,"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"s":{"a":0,"k":[700,700],"ix":2}},{"ty":"st","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Stroke","nm":"Stroke 1","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":13,"ix":5},"c":{"a":0,"k":[0.1059,0.1059,0.1059],"ix":3}},{"ty":"fl","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Fill","nm":"Fill 1","c":{"a":0,"k":[0.1059,0.1059,0.1059],"ix":4},"r":1,"o":{"a":0,"k":0,"ix":5}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[84.571,84.571],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[0,0],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]},{"ty":"tm","bm":0,"hd":false,"mn":"ADBE Vector Filter - Trim","nm":"Trim Paths 1","ix":2,"e":{"a":0,"k":80,"ix":2},"o":{"a":0,"k":0,"ix":3},"s":{"a":0,"k":70,"ix":1},"m":1}],"ind":1},{"ty":4,"nm":"Shape Layer 2","sr":1,"st":0,"op":600.000024438501,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[-100,100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[350,350,0],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":100,"ix":11}},"ef":[],"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Rectangle 1","ix":1,"cix":2,"np":3,"it":[{"ty":"rc","bm":0,"hd":false,"mn":"ADBE Vector Shape - Rect","nm":"Rectangle Path 1","d":1,"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"s":{"a":0,"k":[700,700],"ix":2}},{"ty":"st","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Stroke","nm":"Stroke 1","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":13,"ix":5},"c":{"a":0,"k":[0.1059,0.1059,0.1059],"ix":3}},{"ty":"fl","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Fill","nm":"Fill 1","c":{"a":0,"k":[0.1059,0.1059,0.1059],"ix":4},"r":1,"o":{"a":0,"k":0,"ix":5}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[84.571,84.571],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[0,0],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]},{"ty":"tm","bm":0,"hd":false,"mn":"ADBE Vector Filter - Trim","nm":"Trim Paths 1","ix":2,"e":{"a":0,"k":80,"ix":2},"o":{"a":0,"k":0,"ix":3},"s":{"a":0,"k":70,"ix":1},"m":1}],"ind":2},{"ty":4,"nm":"Shape Layer 3","sr":1,"st":0,"op":600.000024438501,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,-100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[350,350,0],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":100,"ix":11}},"ef":[],"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Rectangle 1","ix":1,"cix":2,"np":3,"it":[{"ty":"rc","bm":0,"hd":false,"mn":"ADBE Vector Shape - Rect","nm":"Rectangle Path 1","d":1,"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"s":{"a":0,"k":[700,700],"ix":2}},{"ty":"st","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Stroke","nm":"Stroke 1","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":13,"ix":5},"c":{"a":0,"k":[0.1059,0.1059,0.1059],"ix":3}},{"ty":"fl","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Fill","nm":"Fill 1","c":{"a":0,"k":[0.1059,0.1059,0.1059],"ix":4},"r":1,"o":{"a":0,"k":0,"ix":5}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[84.571,84.571],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[0,0],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]},{"ty":"tm","bm":0,"hd":false,"mn":"ADBE Vector Filter - Trim","nm":"Trim Paths 1","ix":2,"e":{"a":0,"k":80,"ix":2},"o":{"a":0,"k":0,"ix":3},"s":{"a":0,"k":70,"ix":1},"m":1}],"ind":3},{"ty":4,"nm":"Shape Layer 1","sr":1,"st":0,"op":600.000024438501,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[350,350,0],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":100,"ix":11}},"ef":[],"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Rectangle 1","ix":1,"cix":2,"np":3,"it":[{"ty":"rc","bm":0,"hd":false,"mn":"ADBE Vector Shape - Rect","nm":"Rectangle Path 1","d":1,"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"s":{"a":0,"k":[700,700],"ix":2}},{"ty":"st","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Stroke","nm":"Stroke 1","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":13,"ix":5},"c":{"a":0,"k":[0.1059,0.1059,0.1059],"ix":3}},{"ty":"fl","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Fill","nm":"Fill 1","c":{"a":0,"k":[0.1059,0.1059,0.1059],"ix":4},"r":1,"o":{"a":0,"k":0,"ix":5}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[84.571,84.571],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[0,0],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]},{"ty":"tm","bm":0,"hd":false,"mn":"ADBE Vector Filter - Trim","nm":"Trim Paths 1","ix":2,"e":{"a":0,"k":80,"ix":2},"o":{"a":0,"k":0,"ix":3},"s":{"a":0,"k":70,"ix":1},"m":1}],"ind":4}]}]} \ No newline at end of file