Skip to content

Commit

Permalink
add camera permissions and taking photo
Browse files Browse the repository at this point in the history
  • Loading branch information
rachellefontanilla committed Jul 23, 2024
1 parent af6da56 commit 150ec88
Show file tree
Hide file tree
Showing 6 changed files with 240 additions and 75 deletions.
9 changes: 9 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -122,4 +122,13 @@ dependencies {
testImplementation libs.junit.jupiter.params
debugImplementation libs.androidx.ui.tooling
debugImplementation libs.androidx.ui.test.manifest

// Camera Dependencies
implementation libs.androidx.camera.camera2
implementation libs.androidx.camera.lifecycle
implementation libs.androidx.camera.view

implementation "com.google.accompanist:accompanist-permissions:0.35.1-alpha"


}
1 change: 1 addition & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
android:name="android.hardware.camera"
android:required="false" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

<application
android:name=".CardClarityApplication"
Expand Down
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package edu.card.clarity.presentation.recordReceiptScreen

import android.content.Context
import android.os.Environment
import androidx.camera.core.*
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.view.PreviewView
import androidx.compose.runtime.Composable
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.viewinterop.AndroidView
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.core.content.ContextCompat
import java.io.File
import java.text.SimpleDateFormat
import java.util.*
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.ui.unit.dp
import java.util.concurrent.Executor

@Composable
fun CameraCapture(onImageCaptured: (String) -> Unit, onError: (ImageCaptureException) -> Unit) {
val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current
val cameraProviderFuture = remember { ProcessCameraProvider.getInstance(context) }
val imageCapture = remember { ImageCapture.Builder().build() }
val executor = ContextCompat.getMainExecutor(context) // Define executor here

Box(modifier = Modifier
.fillMaxSize()
.padding(bottom = 50.dp)) {
AndroidView(
factory = { ctx ->
val previewView = PreviewView(ctx)

cameraProviderFuture.addListener({
val cameraProvider = cameraProviderFuture.get()
val preview = Preview.Builder().build().also {
it.setSurfaceProvider(previewView.surfaceProvider)
}

try {
cameraProvider.unbindAll()
cameraProvider.bindToLifecycle(
lifecycleOwner,
CameraSelector.DEFAULT_BACK_CAMERA,
preview,
imageCapture
)
} catch (exc: Exception) {
onError(exc as ImageCaptureException)
}
}, executor)

previewView
},
modifier = Modifier.fillMaxSize()
)

Button(
onClick = {
takePhoto(context, imageCapture, executor, onImageCaptured, onError)
},
modifier = Modifier.align(Alignment.BottomCenter)
) {
Text("Take Picture")
}
}
}

fun takePhoto(
context: Context,
imageCapture: ImageCapture,
executor: Executor,
onImageCaptured: (String) -> Unit,
onError: (ImageCaptureException) -> Unit
) {
val photoFile = createFile(context)
val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build()

imageCapture.takePicture(
outputOptions,
executor,
object : ImageCapture.OnImageSavedCallback {
override fun onImageSaved(output: ImageCapture.OutputFileResults) {
onImageCaptured(photoFile.absolutePath)
}

override fun onError(exc: ImageCaptureException) {
onError(exc)
}
}
)
}

fun createFile(context: Context): File {
val storageDir = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES)
return File(storageDir, SimpleDateFormat("yyyy-MM-dd-HH-mm-ss-SSS", Locale.US)
.format(System.currentTimeMillis()) + ".jpg")
}

Original file line number Diff line number Diff line change
@@ -1,46 +1,53 @@
package edu.card.clarity.presentation.recordReceiptScreen

<<<<<<< HEAD
import android.Manifest
import android.app.DatePickerDialog
import android.graphics.BitmapFactory
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle

import edu.card.clarity.ui.theme.CardClarityTheme
import edu.card.clarity.enums.PurchaseType
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState
import edu.card.clarity.enums.CardNetworkType
import edu.card.clarity.presentation.common.TextField
import edu.card.clarity.presentation.common.DropdownMenu
import edu.card.clarity.enums.PurchaseType
import edu.card.clarity.presentation.common.DatePickerField
import java.util.Calendar
import edu.card.clarity.presentation.common.DropdownMenu
import edu.card.clarity.presentation.common.TextField
import edu.card.clarity.ui.theme.CardClarityTheme
import java.util.*

@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun RecordReceiptScreen(viewModel: RecordReceiptViewModel = hiltViewModel()) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
val uiState by viewModel.uiState.collectAsState()
val context = LocalContext.current
val calendar = Calendar.getInstance()
val showCamera by viewModel.showCamera.collectAsState()

val datePickerDialog = remember {
DatePickerDialog(
context,
{ _, year, month, dayOfMonth ->
viewModel.onDateChange("$year-${month + 1}-$dayOfMonth")
},
calendar.get(Calendar.YEAR),
calendar.get(Calendar.MONTH),
calendar.get(Calendar.DAY_OF_MONTH)
Calendar.getInstance().get(Calendar.YEAR),
Calendar.getInstance().get(Calendar.MONTH),
Calendar.getInstance().get(Calendar.DAY_OF_MONTH)
)
}

val cameraPermissionState = rememberPermissionState(permission = Manifest.permission.CAMERA)

CardClarityTheme {
Column(
modifier = Modifier
Expand All @@ -52,69 +59,98 @@ fun RecordReceiptScreen(viewModel: RecordReceiptViewModel = hiltViewModel()) {
text = "Record a Receipt",
modifier = Modifier.padding(bottom = 16.dp)
)
LazyColumn {
item {
Button(
onClick = viewModel::scanReceipt,
modifier = Modifier.fillMaxWidth()
) {
Text("Scan your receipt")

Spacer(modifier = Modifier.height(16.dp))

if (showCamera) {
CameraCapture(
onImageCaptured = viewModel::onImageCaptured,
onError = { exception ->
// Handle camera errors here, update state to show an error message if necessary
}
Spacer(modifier = Modifier.height(16.dp))
}
item {
Text("Detected information:")
}
item {
DatePickerField(
date = uiState.date,
label = "Date",
onClick = { datePickerDialog.show() }
)
Spacer(modifier = Modifier.height(8.dp))
}
item {
TextField(
label = "Total Amount",
text = uiState.totalAmount,
placeholderText = "Enter total amount",
onTextChange = viewModel::onTotalAmountChange
)
Spacer(modifier = Modifier.height(8.dp))
}
item {
TextField(
label = "Merchant",
text = uiState.merchant,
placeholderText = "Enter merchant",
onTextChange = viewModel::onMerchantChange
)
Spacer(modifier = Modifier.height(8.dp))
}
item {
DropdownMenu(
label = "Card Type",
options = CardNetworkType.entries.map { it.name },
selectedOption = uiState.selectedCard,
onOptionSelected = { viewModel.onCardSelected(CardNetworkType.entries[it].name) }
)
Spacer(modifier = Modifier.height(8.dp))
)
} else {

Button(
onClick = {
if (cameraPermissionState.status.isGranted) {
viewModel.resetCamera()
} else {
cameraPermissionState.launchPermissionRequest()
}
},
modifier = Modifier.fillMaxWidth()
) {
Text("Scan your receipt")
}
item {
DropdownMenu(
label = "Purchase Type",
options = PurchaseType.entries.map { it.name },
selectedOption = uiState.selectedPurchaseType,
onOptionSelected = { viewModel.onPurchaseTypeSelected(PurchaseType.entries[it].name) }

// display receipt
uiState.photoPath?.let { path ->
val imageBitmap = BitmapFactory.decodeFile(path).asImageBitmap()
Image(
bitmap = imageBitmap,
contentDescription = "Captured Receipt",
modifier = Modifier
.fillMaxWidth()
.height(200.dp)
.padding(top = 16.dp)
)
Spacer(modifier = Modifier.height(16.dp))
}
item {
Button(
onClick = viewModel::addReceipt,
modifier = Modifier.fillMaxWidth()
) {
Text("Add Receipt")
LazyColumn {
item {
Text("Detected information:")
}
item {
DatePickerField(
date = uiState.date,
label = "Date",
onClick = { datePickerDialog.show() }
)
Spacer(modifier = Modifier.height(8.dp))
}
item {
TextField(
label = "Total Amount",
text = uiState.totalAmount,
placeholderText = "Enter total amount",
onTextChange = viewModel::onTotalAmountChange
)
Spacer(modifier = Modifier.height(8.dp))
}
item {
TextField(
label = "Merchant",
text = uiState.merchant,
placeholderText = "Enter merchant",
onTextChange = viewModel::onMerchantChange
)
Spacer(modifier = Modifier.height(8.dp))
}
item {
DropdownMenu(
label = "Card Type",
options = CardNetworkType.entries.map { it.name },
selectedOption = uiState.selectedCard,
onOptionSelected = { viewModel.onCardSelected(CardNetworkType.entries[it].name) }
)
Spacer(modifier = Modifier.height(8.dp))
}
item {
DropdownMenu(
label = "Purchase Type",
options = PurchaseType.entries.map { it.name },
selectedOption = uiState.selectedPurchaseType,
onOptionSelected = { viewModel.onPurchaseTypeSelected(PurchaseType.entries[it].name) }
)
Spacer(modifier = Modifier.height(16.dp))
}
item {
Button(
onClick = viewModel::addReceipt,
modifier = Modifier.fillMaxWidth()
) {
Text("Add Receipt")
}
}
}
}
Expand All @@ -136,5 +172,3 @@ fun RecordReceiptScreenPreview() {

RecordReceiptScreen(viewModel = mockViewModel)
}
=======
>>>>>>> e5294ad (update manifest permissions)
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,25 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch

class RecordReceiptViewModel : ViewModel() {
private val _uiState = MutableStateFlow(RecordReceiptUiState())
val uiState: StateFlow<RecordReceiptUiState> = _uiState

private val _showCamera = MutableStateFlow(false)
val showCamera: StateFlow<Boolean> = _showCamera.asStateFlow()

fun onImageCaptured(imagePath: String) {
_uiState.value = _uiState.value.copy(photoPath = imagePath)
_showCamera.value = false
}

fun resetCamera() {
_showCamera.value = true
}

fun scanReceipt() {
// TOOD: Implement receipt scanning logic
viewModelScope.launch {
Expand Down Expand Up @@ -53,5 +66,6 @@ data class RecordReceiptUiState(
val totalAmount: String = "",
val merchant: String = "",
val selectedCard: String = "",
val selectedPurchaseType: String = ""
val selectedPurchaseType: String = "",
val photoPath: String? = null
)

0 comments on commit 150ec88

Please sign in to comment.