added the network check

This commit is contained in:
sathwikcs
2024-12-11 12:29:57 +05:30
parent 58dd00db11
commit 5afbcc6670
3 changed files with 286 additions and 317 deletions

View File

@@ -0,0 +1,197 @@
package `in`.sminnovations.attendanceapp.screens.home.ui.componets
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.KeyboardArrowUp
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedCard
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.PermissionState
import com.google.accompanist.permissions.isGranted
import `in`.sminnovations.attendanceapp.screens.home.viewModel.AttendanceUiState
@OptIn(ExperimentalPermissionsApi::class)
@Composable
internal fun AttendanceScreenContent(
uiState: AttendanceUiState,
cameraPermissionState: PermissionState,
onScannerToggle: () -> Unit,
onQrCodeScanned: (String) -> Unit,
modifier: Modifier = Modifier
) {
Box(modifier = modifier.fillMaxSize()) {
when {
!cameraPermissionState.status.isGranted -> {
RequestPermissionsContent(cameraPermissionState)
}
uiState.isScanning -> {
ScannerContent(
onQrCodeScanned = onQrCodeScanned,
onClose = onScannerToggle
)
}
uiState.errorMessage != null -> {
ErrorContent(
errorMessage = uiState.errorMessage,
userName = uiState.userName,
checkedOutTime = uiState.checkedOutTime
)
}
uiState.isCheckedIn -> {
AttendanceStatusCard(
title = "Checked In as",
userName = uiState.userName
)
}
uiState.isCheckedOut -> {
AttendanceStatusCard(
title = "Checked Out as",
userName = uiState.userName
)
}
}
if (uiState.isLoading) {
LoadingOverlay()
}
ScannerToggleButton(
isScanning = uiState.isScanning,
onClick = onScannerToggle,
modifier = Modifier.align(Alignment.BottomCenter)
)
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun ScannerToggleButton(
isScanning: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
OutlinedCard(
onClick = onClick,
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.primary
),
modifier = modifier
.fillMaxWidth()
.padding(16.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 16.dp),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = if (isScanning) Icons.Default.Close else Icons.Default.KeyboardArrowUp,
contentDescription = null,
modifier = Modifier.padding(end = 8.dp)
)
Text(
text = if (isScanning) "Close Scanner" else "Open Scanner",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
}
}
}
@Composable
private fun LoadingOverlay() {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Black.copy(alpha = 0.3f)),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator(
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(48.dp)
)
}
}
@Composable
private fun AttendanceStatusCard(
title: String,
userName: String?
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
OutlinedCard {
Text(
text = title,
letterSpacing = 2.sp,
lineHeight = 40.sp,
fontSize = 30.sp,
color = Color.Green,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(10.dp, 5.dp)
)
}
Spacer(modifier = Modifier.height(10.dp))
Text(
text = userName ?: "",
letterSpacing = 2.sp,
lineHeight = 40.sp,
fontSize = 30.sp,
fontWeight = FontWeight.Bold
)
}
}
@Composable
private fun ScannerContent(
onQrCodeScanned: (String) -> Unit,
onClose: () -> Unit
) {
Box(modifier = Modifier.fillMaxSize()) {
QRCodeScannerScreen(onQrCodeScanned)
IconButton(
onClick = onClose,
modifier = Modifier
.align(Alignment.TopStart)
.padding(16.dp)
) {
Icon(Icons.Default.ArrowBack, contentDescription = "Close Scanner")
}
}
BackHandler { onClose() }
}

View File

@@ -32,23 +32,20 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.PermissionState
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState
import `in`.sminnovations.attendanceapp.screens.home.ui.componets.AttendanceScreenContent
import `in`.sminnovations.attendanceapp.screens.home.ui.componets.NetworkErrorDialog
import `in`.sminnovations.attendanceapp.screens.home.ui.componets.QRCodeScannerScreen
import `in`.sminnovations.attendanceapp.screens.home.viewModel.AttendanceUiState
import `in`.sminnovations.attendanceapp.screens.home.viewModel.HomeViewModel
@OptIn(ExperimentalPermissionsApi::class, ExperimentalMaterial3Api::class)
@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun HomeScreen(
viewModel: HomeViewModel = hiltViewModel(),
@@ -56,287 +53,20 @@ fun HomeScreen(
) {
val uiState by viewModel.uiState.collectAsState()
val cameraPermissionState = rememberPermissionState(Manifest.permission.CAMERA)
AttendanceScreenContent(
uiState = uiState,
cameraPermissionState = cameraPermissionState,
onScannerToggle = { if (uiState.isScanning) viewModel.stopScanning() else viewModel.startScanning() },
onQrCodeScanned = viewModel::setQRCodeValue,
modifier = modifier
NetworkErrorDialog(
showDialog = !uiState.isConnected,
onRetry = {
viewModel.reStartMonitoring()
}
)
}
@OptIn(ExperimentalPermissionsApi::class)
@Composable
private fun AttendanceScreenContent(
uiState: AttendanceUiState,
cameraPermissionState: PermissionState,
onScannerToggle: () -> Unit,
onQrCodeScanned: (String) -> Unit,
modifier: Modifier = Modifier
) {
Box(modifier = modifier.fillMaxSize()) {
when {
!cameraPermissionState.status.isGranted -> {
RequestPermissionsContent(cameraPermissionState)
}
uiState.isScanning -> {
ScannerContent(
onQrCodeScanned = onQrCodeScanned,
onClose = onScannerToggle
)
}
uiState.errorMessage != null -> {
ErrorContent(
errorMessage = uiState.errorMessage,
userName = uiState.userName,
checkedOutTime = uiState.checkedOutTime
)
}
uiState.isCheckedIn -> {
AttendanceStatusCard(
title = "Checked In as",
userName = uiState.userName
)
}
uiState.isCheckedOut -> {
AttendanceStatusCard(
title = "Checked Out as",
userName = uiState.userName
)
}
}
// Loading overlay
if (uiState.isLoading) {
LoadingOverlay()
}
// Bottom scanner toggle button
ScannerToggleButton(
isScanning = uiState.isScanning,
onClick = onScannerToggle,
modifier = Modifier.align(Alignment.BottomCenter)
if (uiState.isConnected){
AttendanceScreenContent(
uiState = uiState,
cameraPermissionState = cameraPermissionState,
onScannerToggle = { if (uiState.isScanning) viewModel.stopScanning() else viewModel.startScanning() },
onQrCodeScanned = viewModel::setQRCodeValue,
modifier = modifier
)
}
}
@OptIn(ExperimentalPermissionsApi::class)
@Composable
private fun RequestPermissionsContent(cameraPermissionState: PermissionState) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
text = "Camera Permission Required",
style = MaterialTheme.typography.headlineMedium,
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.height(16.dp))
Button(onClick = { cameraPermissionState.launchPermissionRequest() }) {
Text("Request Permission")
}
}
}
@Composable
private fun ScannerContent(
onQrCodeScanned: (String) -> Unit,
onClose: () -> Unit
) {
Box(modifier = Modifier.fillMaxSize()) {
QRCodeScannerScreen(onQrCodeScanned)
IconButton(
onClick = onClose,
modifier = Modifier
.align(Alignment.TopStart)
.padding(16.dp)
) {
Icon(Icons.Default.ArrowBack, contentDescription = "Close Scanner")
}
}
BackHandler { onClose() }
}
@Composable
private fun MainContent(uiState: AttendanceUiState) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
when {
uiState.errorMessage != null -> {
ErrorContent(
errorMessage = uiState.errorMessage,
userName = uiState.userName,
checkedOutTime = uiState.checkedOutTime
)
}
uiState.isCheckedIn -> {
AttendanceStatusCard(
title = "Checked In as",
userName = uiState.userName
)
}
uiState.isCheckedOut -> {
AttendanceStatusCard(
title = "Checked Out as",
userName = uiState.userName
)
}
}
}
}
@Composable
private fun AttendanceStatusCard(
title: String,
userName: String?
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
OutlinedCard {
Text(
text = title,
letterSpacing = 2.sp,
lineHeight = 40.sp,
fontSize = 30.sp,
color = Color.Green,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(10.dp, 5.dp)
)
}
Spacer(modifier = Modifier.height(10.dp))
Text(
text = userName ?: "",
letterSpacing = 2.sp,
lineHeight = 40.sp,
fontSize = 30.sp,
fontWeight = FontWeight.Bold
)
}
}
@Composable
private fun ErrorContent(
errorMessage: String,
userName: String?,
checkedOutTime: String
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(20.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
// Only show the "Error Message for" text if we have a valid userName
if (!userName.isNullOrEmpty()) {
OutlinedCard(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 16.dp)
) {
Text(
text = buildAnnotatedString {
append("Error Message for ")
withStyle(style = SpanStyle(color = Color.Green)) {
append(userName)
}
},
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.padding(16.dp)
)
}
}
OutlinedCard(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = if (checkedOutTime.isNotEmpty()) 16.dp else 0.dp)
) {
Text(
text = errorMessage,
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.error,
modifier = Modifier.padding(16.dp)
)
}
if (checkedOutTime.isNotEmpty()) {
OutlinedCard(
modifier = Modifier.fillMaxWidth()
) {
Column(
modifier = Modifier.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "Check Out Time",
style = MaterialTheme.typography.titleLarge
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = checkedOutTime,
style = MaterialTheme.typography.titleLarge,
color = Color.Green
)
}
}
}
}
}
@Composable
private fun LoadingOverlay() {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Black.copy(alpha = 0.3f)),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator(
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(48.dp)
)
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun ScannerToggleButton(
isScanning: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
OutlinedCard(
onClick = onClick,
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.primary
),
modifier = modifier
.fillMaxWidth()
.padding(16.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 16.dp),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = if (isScanning) Icons.Default.Close else Icons.Default.KeyboardArrowUp,
contentDescription = null,
modifier = Modifier.padding(end = 8.dp)
)
Text(
text = if (isScanning) "Close Scanner" else "Open Scanner",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
}
}
}

View File

@@ -1,17 +1,23 @@
package `in`.sminnovations.attendanceapp.screens.home.viewModel
import android.util.Log
import android.content.Context
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.asFlow
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.viewModel
import com.google.firebase.Timestamp
import com.google.firebase.firestore.DocumentSnapshot
import com.google.firebase.firestore.FirebaseFirestore
import dagger.hilt.android.lifecycle.HiltViewModel
import `in`.sminnovations.attendanceapp.main.NetworkMonitor
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.tasks.await
@@ -19,18 +25,42 @@ import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import javax.inject.Inject
@HiltViewModel
class HomeViewModel @Inject constructor(
private val applicationContext: Context,
private val firebaseFirestore: FirebaseFirestore,
private val networkMonitor: NetworkMonitor
) : ViewModel() {
private val _uiState = MutableStateFlow(AttendanceUiState())
val uiState: StateFlow<AttendanceUiState> = _uiState.asStateFlow()
init {
viewModelScope.launch {
startScanning()
networkMonitor.startMonitoring()
networkMonitor.isConnected.asFlow()
.onEach { isConnected ->
_uiState.update { currentState ->
currentState.copy(isConnected = isConnected)
}
}
.launchIn(viewModelScope)
if (uiState.value.isConnected) {
viewModelScope.launch {
startScanning()
}
}
}
fun stopMonitoring() {
networkMonitor.stopMonitoring()
}
fun reStartMonitoring() {
networkMonitor.stopMonitoring()
networkMonitor.startMonitoring()
}
fun startScanning() {
@@ -43,13 +73,12 @@ class HomeViewModel @Inject constructor(
}
}
fun stopScanning() {
viewModelScope.launch {
_uiState.update { currentState ->
currentState.copy(
isScanning = false,
errorMessage = null,
qrCodeValue = null
isScanning = false, errorMessage = null, qrCodeValue = null
)
}
}
@@ -77,8 +106,7 @@ class HomeViewModel @Inject constructor(
setLoading(true)
_uiState.update {
it.copy(
errorMessage = null,
userName = null // Reset userName before processing
errorMessage = null, userName = null // Reset userName before processing
)
}
@@ -138,11 +166,11 @@ class HomeViewModel @Inject constructor(
checkOut != null -> {
_uiState.update {
it.copy(
checkedOutTime = checkOut,
errorMessage = "Already checked out for today."
checkedOutTime = checkOut, errorMessage = "Already checked out for today."
)
}
}
else -> handleError("Invalid attendance record")
}
}
@@ -155,15 +183,12 @@ class HomeViewModel @Inject constructor(
"checkOut" to "NA"
)
firebaseFirestore.collection("attendance_$userId")
.document(currentDate)
.set(checkInData)
.await()
firebaseFirestore.collection("attendance_$userId").document(currentDate)
.set(checkInData).await()
_uiState.update {
it.copy(
isCheckedIn = true,
qrCodeValue = null
isCheckedIn = true, qrCodeValue = null
)
}
} catch (e: Exception) {
@@ -174,16 +199,14 @@ class HomeViewModel @Inject constructor(
private suspend fun processCheckOut(userId: String, document: DocumentSnapshot) {
try {
val checkOutData = hashMapOf(
"checkOut" to formatTimestamp(Timestamp.now()),
"DayCompleted" to true
"checkOut" to formatTimestamp(Timestamp.now()), "DayCompleted" to true
)
document.reference.update(checkOutData as Map<String, Any>).await()
_uiState.update {
it.copy(
isCheckedOut = true,
qrCodeValue = null
isCheckedOut = true, qrCodeValue = null
)
}
} catch (e: Exception) {
@@ -193,10 +216,8 @@ class HomeViewModel @Inject constructor(
private suspend fun getEmployeeDocument(userId: String): DocumentSnapshot? {
return try {
val querySnapshot = firebaseFirestore.collection("employees")
.whereEqualTo("Id", userId)
.get()
.await()
val querySnapshot =
firebaseFirestore.collection("employees").whereEqualTo("Id", userId).get().await()
if (querySnapshot.isEmpty) null else querySnapshot.documents.first()
} catch (e: Exception) {
@@ -206,10 +227,7 @@ class HomeViewModel @Inject constructor(
private suspend fun getAttendanceDocument(userId: String, date: String): DocumentSnapshot? {
return try {
firebaseFirestore.collection("attendance_$userId")
.document(date)
.get()
.await()
firebaseFirestore.collection("attendance_$userId").document(date).get().await()
} catch (e: Exception) {
throw Exception("Failed to fetch attendance data")
}
@@ -227,9 +245,32 @@ class HomeViewModel @Inject constructor(
}
private fun formatTimestamp(timestamp: Timestamp): String {
return SimpleDateFormat("dd MMMM yyyy hh:mm:ss a", Locale.getDefault())
.format(timestamp.toDate())
return SimpleDateFormat(
"dd MMMM yyyy hh:mm:ss a",
Locale.getDefault()
).format(timestamp.toDate())
}
fun isDeviceConnectedToInternet(context: Context): Boolean {
val connectivityManager =
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val network = connectivityManager.activeNetwork ?: return false
val networkCapabilities =
connectivityManager.getNetworkCapabilities(network) ?: return false
return when {
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> true
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> true
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> true
else -> false
}
}
override fun onCleared() {
super.onCleared()
networkMonitor.stopMonitoring()
}
}
data class AttendanceUiState(
@@ -240,5 +281,6 @@ data class AttendanceUiState(
val isCheckedOut: Boolean = false,
val checkedOutTime: String = "",
val errorMessage: String? = null,
val isLoading: Boolean = false
val isLoading: Boolean = false,
val isConnected : Boolean =true
)