From 5afbcc6670c886eda71186b9444b3f6194a68599 Mon Sep 17 00:00:00 2001 From: sathwikcs <85106054+sathwikcs@users.noreply.github.com> Date: Wed, 11 Dec 2024 12:29:57 +0530 Subject: [PATCH] added the network check --- .../ui/componets/AttendanceScreenContent.kt | 197 ++++++++++++ .../attendanceapp/screens/home/ui/home.kt | 300 +----------------- .../screens/home/viewModel/homeVM.kt | 106 +++++-- 3 files changed, 286 insertions(+), 317 deletions(-) create mode 100644 app/src/main/java/in/sminnovations/attendanceapp/screens/home/ui/componets/AttendanceScreenContent.kt diff --git a/app/src/main/java/in/sminnovations/attendanceapp/screens/home/ui/componets/AttendanceScreenContent.kt b/app/src/main/java/in/sminnovations/attendanceapp/screens/home/ui/componets/AttendanceScreenContent.kt new file mode 100644 index 0000000..7144ec5 --- /dev/null +++ b/app/src/main/java/in/sminnovations/attendanceapp/screens/home/ui/componets/AttendanceScreenContent.kt @@ -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() } +} + + diff --git a/app/src/main/java/in/sminnovations/attendanceapp/screens/home/ui/home.kt b/app/src/main/java/in/sminnovations/attendanceapp/screens/home/ui/home.kt index 42f1f50..24314f7 100644 --- a/app/src/main/java/in/sminnovations/attendanceapp/screens/home/ui/home.kt +++ b/app/src/main/java/in/sminnovations/attendanceapp/screens/home/ui/home.kt @@ -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 - ) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/in/sminnovations/attendanceapp/screens/home/viewModel/homeVM.kt b/app/src/main/java/in/sminnovations/attendanceapp/screens/home/viewModel/homeVM.kt index c260bb3..c91c676 100644 --- a/app/src/main/java/in/sminnovations/attendanceapp/screens/home/viewModel/homeVM.kt +++ b/app/src/main/java/in/sminnovations/attendanceapp/screens/home/viewModel/homeVM.kt @@ -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 = _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).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 ) \ No newline at end of file