From 7a8cad905238cacd2148c59da62b4aeb099c19db Mon Sep 17 00:00:00 2001 From: Dev Keshwani Date: Sun, 3 Aug 2025 12:40:09 +0530 Subject: [PATCH 1/4] chore: update version to 3.0.1 and modify support contact details --- app/build.gradle | 4 ++-- app/src/main/res/values/strings.xml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 816d9c0..47e6dca 100755 --- a/app/build.gradle +++ b/app/build.gradle @@ -35,8 +35,8 @@ android { namespace "com.dscvit.vitty" minSdkVersion 26 targetSdkVersion 36 - versionCode 41 - versionName "3.0.0" + versionCode 43 + versionName "3.0.1" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" signingConfig signingConfigs.release } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 844eecd..0bf6f78 100755 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -88,8 +88,8 @@ con.dscvit.vitty-Updates Directions Facing any issues? Contact us! - We are available on Telegram throughout the day ready to fix your problems! - https://dscv.it/telegram + Reach out support in case of any issues! + https://vitty.dscvit.com Support SJT101 Academics From e1420c5246edcc7a9d18cff1401c83c634ddaa8d Mon Sep 17 00:00:00 2001 From: Dev Keshwani Date: Mon, 4 Aug 2025 01:59:06 +0530 Subject: [PATCH 2/4] feat: implement maintenance mode with dedicated activity and UI components --- app/src/main/AndroidManifest.xml | 3 + .../com/dscvit/vitty/activity/AuthActivity.kt | 47 ++- .../vitty/activity/MaintenanceActivity.kt | 52 ++++ .../dscvit/vitty/ui/main/MainComposeApp.kt | 36 ++- .../vitty/ui/maintenance/MaintenanceBanner.kt | 270 ++++++++++++++++++ .../vitty/ui/maintenance/MaintenanceScreen.kt | 115 ++++++++ .../dscvit/vitty/util/MaintenanceChecker.kt | 78 +++++ app/src/main/res/drawable/ic_maintenance.xml | 10 + app/src/main/res/drawable/ic_screwdriver.xml | 10 + .../main/res/layout/activity_maintenance.xml | 22 ++ app/src/main/res/values/strings.xml | 8 + 11 files changed, 637 insertions(+), 14 deletions(-) create mode 100644 app/src/main/java/com/dscvit/vitty/activity/MaintenanceActivity.kt create mode 100644 app/src/main/java/com/dscvit/vitty/ui/maintenance/MaintenanceBanner.kt create mode 100644 app/src/main/java/com/dscvit/vitty/ui/maintenance/MaintenanceScreen.kt create mode 100644 app/src/main/java/com/dscvit/vitty/util/MaintenanceChecker.kt create mode 100644 app/src/main/res/drawable/ic_maintenance.xml create mode 100644 app/src/main/res/drawable/ic_screwdriver.xml create mode 100644 app/src/main/res/layout/activity_maintenance.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 39cb9bf..9f8938b 100755 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -38,6 +38,9 @@ + diff --git a/app/src/main/java/com/dscvit/vitty/activity/AuthActivity.kt b/app/src/main/java/com/dscvit/vitty/activity/AuthActivity.kt index 7882045..a58db1c 100755 --- a/app/src/main/java/com/dscvit/vitty/activity/AuthActivity.kt +++ b/app/src/main/java/com/dscvit/vitty/activity/AuthActivity.kt @@ -10,6 +10,7 @@ import android.view.View import android.widget.Toast import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts +import androidx.activity.OnBackPressedCallback import androidx.appcompat.app.AppCompatActivity import androidx.core.content.edit import androidx.databinding.DataBindingUtil @@ -24,6 +25,7 @@ import com.dscvit.vitty.util.Constants.TOKEN import com.dscvit.vitty.util.Constants.UID import com.dscvit.vitty.util.Constants.USER_INFO import com.dscvit.vitty.util.NotificationPermissionHelper +import com.dscvit.vitty.util.MaintenanceChecker import com.google.android.gms.auth.api.signin.GoogleSignIn import com.google.android.gms.auth.api.signin.GoogleSignInAccount import com.google.android.gms.auth.api.signin.GoogleSignInClient @@ -62,6 +64,22 @@ class AuthActivity : AppCompatActivity() { configureGoogleSignIn() setupUI() + setupBackPressedHandler() + } + + private fun setupBackPressedHandler() { + val callback = object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + binding.apply { + if (introPager.currentItem == 0 || loginClick) { + finish() + } else { + introPager.currentItem-- + } + } + } + } + onBackPressedDispatcher.addCallback(this, callback) } private fun setupNotificationPermissionLauncher() { @@ -266,8 +284,9 @@ class AuthActivity : AppCompatActivity() { val email = firebaseAuth.currentUser?.email Timber.d("Firebase authentication successful - uid: $uid, email: $email") saveInfo(acct.idToken, uid) - authViewModel.signInAndGetTimeTable("", "", uid ?: "", "") - leadToNextPage() + + // Quick maintenance check for new login + checkMaintenanceBeforeProceed() } else { Timber.e("Firebase authentication failed: ${authResult.exception?.message}") logoutFailed() @@ -278,6 +297,20 @@ class AuthActivity : AppCompatActivity() { } } + private fun checkMaintenanceBeforeProceed() { + MaintenanceChecker.checkMaintenanceStatusAsync(this) { isUnderMaintenance -> + if (isUnderMaintenance) { + binding.loadingView.visibility = View.GONE + val intent = Intent(this, MaintenanceActivity::class.java) + startActivity(intent) + finish() + } else { + authViewModel.signInAndGetTimeTable("", "", firebaseAuth.currentUser?.uid ?: "", "") + leadToNextPage() + } + } + } + private fun leadToNextPage() { authViewModel.signInResponse.observe(this) { if (it != null) { @@ -323,14 +356,4 @@ class AuthActivity : AppCompatActivity() { } } } - - override fun onBackPressed() { - binding.apply { - if (introPager.currentItem == 0 || loginClick) { - super.onBackPressed() - } else { - introPager.currentItem-- - } - } - } } diff --git a/app/src/main/java/com/dscvit/vitty/activity/MaintenanceActivity.kt b/app/src/main/java/com/dscvit/vitty/activity/MaintenanceActivity.kt new file mode 100644 index 0000000..1b4fef1 --- /dev/null +++ b/app/src/main/java/com/dscvit/vitty/activity/MaintenanceActivity.kt @@ -0,0 +1,52 @@ +package com.dscvit.vitty.activity + +import android.content.Intent +import android.os.Bundle +import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.databinding.DataBindingUtil +import androidx.fragment.app.FragmentActivity +import com.dscvit.vitty.R +import com.dscvit.vitty.databinding.ActivityMaintenanceBinding +import com.dscvit.vitty.theme.VittyTheme +import com.dscvit.vitty.ui.maintenance.MaintenanceScreen +import com.dscvit.vitty.util.MaintenanceChecker + +class MaintenanceActivity : FragmentActivity() { + + private lateinit var binding: ActivityMaintenanceBinding + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + binding = DataBindingUtil.setContentView(this, R.layout.activity_maintenance) + + binding.composeView.apply { + setViewCompositionStrategy( + ViewCompositionStrategy.DisposeOnLifecycleDestroyed(this@MaintenanceActivity) + ) + setContent { + VittyTheme { + MaintenanceScreen( + onRetryClick = { retryConnection() }, + onExitClick = { exitApp() } + ) + } + } + } + } + + private fun retryConnection() { + MaintenanceChecker.checkMaintenanceStatusAsync(this) { isUnderMaintenance -> + if (!isUnderMaintenance) { + val intent = Intent(this, InstructionsActivity::class.java) + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + startActivity(intent) + finish() + } + } + } + + private fun exitApp() { + finishAffinity() + } +} diff --git a/app/src/main/java/com/dscvit/vitty/ui/main/MainComposeApp.kt b/app/src/main/java/com/dscvit/vitty/ui/main/MainComposeApp.kt index 0437040..5fc1a86 100644 --- a/app/src/main/java/com/dscvit/vitty/ui/main/MainComposeApp.kt +++ b/app/src/main/java/com/dscvit/vitty/ui/main/MainComposeApp.kt @@ -103,8 +103,10 @@ import com.dscvit.vitty.ui.schedule.ScheduleViewModel import com.dscvit.vitty.util.Analytics import com.dscvit.vitty.util.Constants import com.dscvit.vitty.util.LogoutHelper +import com.dscvit.vitty.util.MaintenanceChecker import com.dscvit.vitty.util.SemesterUtils import com.dscvit.vitty.util.UtilFunctions +import com.dscvit.vitty.ui.maintenance.MaintenanceBannerDialog import com.google.gson.Gson import com.google.gson.reflect.TypeToken import java.net.URLDecoder @@ -129,6 +131,10 @@ fun MainComposeApp() { var campus by remember { mutableStateOf(prefs.getString(Constants.COMMUNITY_CAMPUS, "") ?: "") } var showCampusDialog by remember { mutableStateOf(campus.isEmpty()) } + + // Maintenance banner state + var showMaintenanceBanner by remember { mutableStateOf(false) } + var lastMaintenanceCheck by remember { mutableStateOf(0L) } DisposableEffect(Unit) { val listener = @@ -208,13 +214,25 @@ fun MainComposeApp() { } } + LaunchedEffect(Unit) { + if (MaintenanceChecker.isNetworkAvailable(context)) { + MaintenanceChecker.checkMaintenanceStatusAsync(context) { isUnderMaintenance -> + android.util.Log.d("MaintenanceCheck", "Dialog check result: isUnderMaintenance=$isUnderMaintenance") + showMaintenanceBanner = isUnderMaintenance + lastMaintenanceCheck = System.currentTimeMillis() + } + } else { + android.util.Log.d("MaintenanceCheck", "No network available") + } + } + VittyTheme { ModalNavigationDrawer( drawerState = drawerState, drawerContent = { DrawerContent( navController = navController, - onCloseDrawer = { scope.launch { drawerState.close() } }, + onCloseDrawer = { scope.launch { drawerState.close() } } ) }, ) { @@ -866,6 +884,20 @@ fun MainComposeApp() { onDismiss = { showCampusDialog = false }, ) } + + // Maintenance Banner Dialog + MaintenanceBannerDialog( + isVisible = showMaintenanceBanner, + onDismiss = { + showMaintenanceBanner = false + }, + onRetryClick = { + MaintenanceChecker.checkMaintenanceStatusAsync(context) { isUnderMaintenance -> + showMaintenanceBanner = isUnderMaintenance + lastMaintenanceCheck = System.currentTimeMillis() + } + } + ) } } } @@ -1091,7 +1123,7 @@ fun NavigationItem( @Composable fun DrawerContent( navController: NavHostController, - onCloseDrawer: () -> Unit, + onCloseDrawer: () -> Unit ) { val context = LocalContext.current val prefs = remember { context.getSharedPreferences(Constants.USER_INFO, 0) } diff --git a/app/src/main/java/com/dscvit/vitty/ui/maintenance/MaintenanceBanner.kt b/app/src/main/java/com/dscvit/vitty/ui/maintenance/MaintenanceBanner.kt new file mode 100644 index 0000000..161adc5 --- /dev/null +++ b/app/src/main/java/com/dscvit/vitty/ui/maintenance/MaintenanceBanner.kt @@ -0,0 +1,270 @@ +package com.dscvit.vitty.ui.maintenance + +import androidx.compose.animation.* +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.Warning +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import com.dscvit.vitty.R +import com.dscvit.vitty.theme.* + +@Composable +fun MaintenanceBannerDialog( + isVisible: Boolean, + onDismiss: () -> Unit, + onRetryClick: () -> Unit +) { + if (isVisible) { + Dialog( + onDismissRequest = onDismiss, + properties = DialogProperties( + dismissOnBackPress = true, + dismissOnClickOutside = true + ) + ) { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + shape = RoundedCornerShape(20.dp), + colors = CardDefaults.cardColors( + containerColor = Background + ), + elevation = CardDefaults.cardElevation( + defaultElevation = 8.dp + ) + ) { + Column( + modifier = Modifier.padding(20.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + // Header with close button + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + IconButton( + onClick = onDismiss, + modifier = Modifier.size(24.dp) + ) { + Icon( + imageVector = Icons.Default.Close, + contentDescription = "Close", + tint = Accent.copy(alpha = 0.7f), + modifier = Modifier.size(20.dp) + ) + } + } + + Spacer(modifier = Modifier.height(16.dp)) + + // Maintenance icon with gradient background + Box( + modifier = Modifier + .size(80.dp) + .clip(RoundedCornerShape(16.dp)) + .background( + brush = Brush.radialGradient( + colors = listOf( + Accent.copy(alpha = 0.2f), + Accent.copy(alpha = 0.1f) + ) + ) + ), + contentAlignment = Alignment.Center + ) { + Icon( + painter = painterResource(id = R.drawable.ic_maintenance), + contentDescription = "Maintenance", + modifier = Modifier.size(40.dp), + tint = Accent + ) + } + + Spacer(modifier = Modifier.height(16.dp)) + + // Title + Text( + text = "Server Maintenance", + fontSize = 22.sp, + fontWeight = FontWeight.Bold, + color = TextColor, + textAlign = TextAlign.Center + ) + + Spacer(modifier = Modifier.height(8.dp)) + + // Message + Text( + text = "The server is currently under maintenance. Some features may be temporarily unavailable.", + fontSize = 14.sp, + color = Accent.copy(alpha = 0.8f), + textAlign = TextAlign.Center, + lineHeight = 18.sp + ) + + Spacer(modifier = Modifier.height(20.dp)) + + // Action buttons + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + // Retry button + Button( + onClick = onRetryClick, + modifier = Modifier.weight(1f), + colors = ButtonDefaults.buttonColors( + containerColor = Accent, + contentColor = Background + ), + shape = RoundedCornerShape(12.dp) + ) { + Text( + text = "Retry", + fontSize = 14.sp, + fontWeight = FontWeight.Medium + ) + } + + // Dismiss button + OutlinedButton( + onClick = onDismiss, + modifier = Modifier.weight(1f), + colors = ButtonDefaults.outlinedButtonColors( + contentColor = Accent.copy(alpha = 0.8f) + ), + shape = RoundedCornerShape(12.dp) + ) { + Text( + text = "Continue", + fontSize = 14.sp, + fontWeight = FontWeight.Medium + ) + } + } + + Spacer(modifier = Modifier.height(8.dp)) + + // Footer + Text( + text = "Thank you for your patience", + fontSize = 12.sp, + color = Accent.copy(alpha = 0.6f), + textAlign = TextAlign.Center, + fontWeight = FontWeight.Medium + ) + } + } + } + } +} + +@Composable +fun MaintenanceBanner( + isVisible: Boolean, + onDismiss: () -> Unit, + onRetryClick: () -> Unit +) { + AnimatedVisibility( + visible = isVisible, + enter = slideInVertically( + initialOffsetY = { -it } + ) + fadeIn(), + exit = slideOutVertically( + targetOffsetY = { -it } + ) + fadeOut() + ) { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + shape = RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors( + containerColor = Secondary.copy(alpha = 0.95f) + ), + elevation = CardDefaults.cardElevation( + defaultElevation = 6.dp + ) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + // Icon + Icon( + painter = painterResource(id = R.drawable.ic_maintenance), + contentDescription = "Maintenance", + modifier = Modifier.size(24.dp), + tint = Accent + ) + + Spacer(modifier = Modifier.width(12.dp)) + + // Text content + Column( + modifier = Modifier.weight(1f) + ) { + Text( + text = "Server Maintenance", + fontSize = 14.sp, + fontWeight = FontWeight.SemiBold, + color = Accent + ) + Text( + text = "Some features may be limited", + fontSize = 12.sp, + color = Accent.copy(alpha = 0.7f) + ) + } + + // Action buttons + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + TextButton( + onClick = onRetryClick, + contentPadding = PaddingValues(horizontal = 8.dp, vertical = 4.dp) + ) { + Text( + text = "Retry", + fontSize = 12.sp, + color = Accent, + fontWeight = FontWeight.Medium + ) + } + + IconButton( + onClick = onDismiss, + modifier = Modifier.size(24.dp) + ) { + Icon( + imageVector = Icons.Default.Close, + contentDescription = "Dismiss", + tint = Accent.copy(alpha = 0.7f), + modifier = Modifier.size(16.dp) + ) + } + } + } + } + } +} diff --git a/app/src/main/java/com/dscvit/vitty/ui/maintenance/MaintenanceScreen.kt b/app/src/main/java/com/dscvit/vitty/ui/maintenance/MaintenanceScreen.kt new file mode 100644 index 0000000..429d00d --- /dev/null +++ b/app/src/main/java/com/dscvit/vitty/ui/maintenance/MaintenanceScreen.kt @@ -0,0 +1,115 @@ +package com.dscvit.vitty.ui.maintenance + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.dscvit.vitty.R +import com.dscvit.vitty.theme.* + +@Composable +fun MaintenanceScreen( + onRetryClick: () -> Unit, + onExitClick: () -> Unit +) { + Box( + modifier = Modifier + .fillMaxSize() + .padding(32.dp), + contentAlignment = Alignment.Center + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Image( + painter = painterResource(id = R.drawable.ic_maintenance), + contentDescription = stringResource(R.string.maintenance_icon_desc), + modifier = Modifier.size(120.dp), + colorFilter = ColorFilter.tint(Accent) + ) + + Spacer(modifier = Modifier.height(32.dp)) + + Text( + text = stringResource(R.string.maintenance_title), + fontSize = 28.sp, + fontWeight = FontWeight.SemiBold, + color = TextColor, + textAlign = TextAlign.Center, + lineHeight = 36.sp + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = stringResource(R.string.maintenance_message), + fontSize = 16.sp, + color = Accent, + textAlign = TextAlign.Center, + lineHeight = 20.sp + ) + + Spacer(modifier = Modifier.height(24.dp)) + + Text( + text = stringResource(R.string.maintenance_description), + fontSize = 14.sp, + color = Accent.copy(alpha = 0.8f), + textAlign = TextAlign.Center, + lineHeight = 16.sp + ) + + Spacer(modifier = Modifier.height(48.dp)) + + Button( + onClick = onRetryClick, + modifier = Modifier + .fillMaxWidth() + .height(56.dp), + colors = ButtonDefaults.buttonColors( + containerColor = Accent, + contentColor = Background + ), + shape = RoundedCornerShape(12.dp) + ) { + Text( + text = stringResource(R.string.retry_connection), + fontSize = 16.sp, + fontWeight = FontWeight.Medium + ) + } + + Spacer(modifier = Modifier.height(16.dp)) + + TextButton( + onClick = onExitClick + ) { + Text( + text = stringResource(R.string.exit_app), + fontSize = 14.sp, + color = Accent.copy(alpha = 0.7f) + ) + } + + Spacer(modifier = Modifier.height(32.dp)) + + Text( + text = stringResource(R.string.maintenance_footer), + fontSize = 12.sp, + color = Accent.copy(alpha = 0.6f), + textAlign = TextAlign.Center + ) + } + } +} diff --git a/app/src/main/java/com/dscvit/vitty/util/MaintenanceChecker.kt b/app/src/main/java/com/dscvit/vitty/util/MaintenanceChecker.kt new file mode 100644 index 0000000..d492282 --- /dev/null +++ b/app/src/main/java/com/dscvit/vitty/util/MaintenanceChecker.kt @@ -0,0 +1,78 @@ +package com.dscvit.vitty.util + +import android.content.Context +import com.dscvit.vitty.util.WebConstants.COMMUNITY_BASE_URL +import kotlinx.coroutines.* +import okhttp3.OkHttpClient +import okhttp3.Request +import timber.log.Timber +import java.util.concurrent.TimeUnit + +object MaintenanceChecker { + + private const val MAINTENANCE_CHECK_TIMEOUT = 5L + private const val API_OPERATIONAL_MESSAGE = "Welcome to VITTY API!🎉" + + private var isChecking = false + + fun checkMaintenanceStatusAsync( + context: Context, + onResult: (isUnderMaintenance: Boolean) -> Unit + ) { + if (isChecking) return + + isChecking = true + + CoroutineScope(Dispatchers.IO).launch { + val isUnderMaintenance = try { + val client = OkHttpClient.Builder() + .connectTimeout(MAINTENANCE_CHECK_TIMEOUT, TimeUnit.SECONDS) + .readTimeout(MAINTENANCE_CHECK_TIMEOUT, TimeUnit.SECONDS) + .callTimeout(MAINTENANCE_CHECK_TIMEOUT, TimeUnit.SECONDS) + .build() + + val request = Request.Builder() + .url(COMMUNITY_BASE_URL) + .build() + + val response = client.newCall(request).execute() + val responseBody = response.body?.string() + + val result = when { + !response.isSuccessful -> { + Timber.d("Server returned error code: ${response.code}") + true // Server error = maintenance + } + responseBody == null -> { + Timber.d("No response body received") + true // No response = maintenance + } + responseBody.contains(API_OPERATIONAL_MESSAGE) -> { + Timber.d("API operational message found") + false // API working + } + else -> { + Timber.d("Different response received: $responseBody") + true // Different response = maintenance + } + } + + Timber.d("Maintenance check: isUnderMaintenance=$result, response=$responseBody") + result + + } catch (e: Exception) { + Timber.e(e, "Maintenance check failed") + false // Network error = don't assume maintenance, just fail silently + } + + withContext(Dispatchers.Main) { + isChecking = false + onResult(isUnderMaintenance) + } + } + } + + fun isNetworkAvailable(context: Context): Boolean { + return UtilFunctions.isNetworkAvailable(context) + } +} diff --git a/app/src/main/res/drawable/ic_maintenance.xml b/app/src/main/res/drawable/ic_maintenance.xml new file mode 100644 index 0000000..8554ac4 --- /dev/null +++ b/app/src/main/res/drawable/ic_maintenance.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_screwdriver.xml b/app/src/main/res/drawable/ic_screwdriver.xml new file mode 100644 index 0000000..8554ac4 --- /dev/null +++ b/app/src/main/res/drawable/ic_screwdriver.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/activity_maintenance.xml b/app/src/main/res/layout/activity_maintenance.xml new file mode 100644 index 0000000..3475e7a --- /dev/null +++ b/app/src/main/res/layout/activity_maintenance.xml @@ -0,0 +1,22 @@ + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0bf6f78..882d931 100755 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -132,4 +132,12 @@ Hello blank fragment + + Server Under Maintenance + We\'re currently performing server maintenance to improve your experience. + Please check back in a few minutes. We\'ll be back online soon! + Try Again + Exit App + Thank you for your patience + Maintenance Icon From 515785d6159fd215b2ae74a02a88aabaacecd2d6 Mon Sep 17 00:00:00 2001 From: Dev Keshwani Date: Mon, 4 Aug 2025 12:19:06 +0530 Subject: [PATCH 3/4] refactor: server status check functionality and listener interface --- .../network/api/community/APICommunity.kt | 3 + .../api/community/APICommunityRestClient.kt | 26 +++++ .../community/RetrofitCommunityListener.kt | 12 +++ .../dscvit/vitty/ui/main/MainComposeApp.kt | 10 +- .../dscvit/vitty/util/MaintenanceChecker.kt | 99 ++++++++----------- 5 files changed, 84 insertions(+), 66 deletions(-) diff --git a/app/src/main/java/com/dscvit/vitty/network/api/community/APICommunity.kt b/app/src/main/java/com/dscvit/vitty/network/api/community/APICommunity.kt index bbc5bfe..f3a0380 100644 --- a/app/src/main/java/com/dscvit/vitty/network/api/community/APICommunity.kt +++ b/app/src/main/java/com/dscvit/vitty/network/api/community/APICommunity.kt @@ -29,6 +29,9 @@ import retrofit2.http.Path import retrofit2.http.Query interface APICommunity { + @GET("/") + fun checkServerStatus(): Call + @Headers("Content-Type: application/json") @POST("/api/v3/auth/check-username") fun checkUsername( diff --git a/app/src/main/java/com/dscvit/vitty/network/api/community/APICommunityRestClient.kt b/app/src/main/java/com/dscvit/vitty/network/api/community/APICommunityRestClient.kt index d9ef161..5ef302a 100644 --- a/app/src/main/java/com/dscvit/vitty/network/api/community/APICommunityRestClient.kt +++ b/app/src/main/java/com/dscvit/vitty/network/api/community/APICommunityRestClient.kt @@ -1050,4 +1050,30 @@ class APICommunityRestClient { }, ) } + + fun checkServerStatus( + retrofitServerStatusListener: RetrofitServerStatusListener, + ) { + mApiUser = retrofit.create(APICommunity::class.java) + val apiServerStatusCall = mApiUser!!.checkServerStatus() + apiServerStatusCall.enqueue( + object : Callback { + override fun onResponse( + call: Call, + response: Response, + ) { + Timber.d("ServerStatus: ${response.body()}") + retrofitServerStatusListener.onSuccess(call, response.body(), response.isSuccessful) + } + + override fun onFailure( + call: Call, + t: Throwable, + ) { + Timber.d("ServerStatusError: ${t.message}") + retrofitServerStatusListener.onError(call, t) + } + }, + ) + } } diff --git a/app/src/main/java/com/dscvit/vitty/network/api/community/RetrofitCommunityListener.kt b/app/src/main/java/com/dscvit/vitty/network/api/community/RetrofitCommunityListener.kt index 41ab0b8..f178876 100644 --- a/app/src/main/java/com/dscvit/vitty/network/api/community/RetrofitCommunityListener.kt +++ b/app/src/main/java/com/dscvit/vitty/network/api/community/RetrofitCommunityListener.kt @@ -169,3 +169,15 @@ interface RetrofitActiveFriendsListener { t: Throwable, ) } + +interface RetrofitServerStatusListener { + fun onSuccess( + call: Call, + response: String?, + isSuccessful: Boolean + ) + fun onError( + call: Call, + t: Throwable + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/dscvit/vitty/ui/main/MainComposeApp.kt b/app/src/main/java/com/dscvit/vitty/ui/main/MainComposeApp.kt index 5fc1a86..58d4ad1 100644 --- a/app/src/main/java/com/dscvit/vitty/ui/main/MainComposeApp.kt +++ b/app/src/main/java/com/dscvit/vitty/ui/main/MainComposeApp.kt @@ -115,6 +115,7 @@ import java.nio.charset.StandardCharsets import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import timber.log.Timber @Composable fun MainComposeApp() { @@ -134,7 +135,6 @@ fun MainComposeApp() { // Maintenance banner state var showMaintenanceBanner by remember { mutableStateOf(false) } - var lastMaintenanceCheck by remember { mutableStateOf(0L) } DisposableEffect(Unit) { val listener = @@ -215,14 +215,13 @@ fun MainComposeApp() { } LaunchedEffect(Unit) { - if (MaintenanceChecker.isNetworkAvailable(context)) { + if (UtilFunctions.isNetworkAvailable(context)) { MaintenanceChecker.checkMaintenanceStatusAsync(context) { isUnderMaintenance -> - android.util.Log.d("MaintenanceCheck", "Dialog check result: isUnderMaintenance=$isUnderMaintenance") + Timber.d("Dialog check result: isUnderMaintenance=%s", isUnderMaintenance) showMaintenanceBanner = isUnderMaintenance - lastMaintenanceCheck = System.currentTimeMillis() } } else { - android.util.Log.d("MaintenanceCheck", "No network available") + Timber.d("No network available") } } @@ -894,7 +893,6 @@ fun MainComposeApp() { onRetryClick = { MaintenanceChecker.checkMaintenanceStatusAsync(context) { isUnderMaintenance -> showMaintenanceBanner = isUnderMaintenance - lastMaintenanceCheck = System.currentTimeMillis() } } ) diff --git a/app/src/main/java/com/dscvit/vitty/util/MaintenanceChecker.kt b/app/src/main/java/com/dscvit/vitty/util/MaintenanceChecker.kt index d492282..e03b6f3 100644 --- a/app/src/main/java/com/dscvit/vitty/util/MaintenanceChecker.kt +++ b/app/src/main/java/com/dscvit/vitty/util/MaintenanceChecker.kt @@ -1,78 +1,57 @@ package com.dscvit.vitty.util import android.content.Context -import com.dscvit.vitty.util.WebConstants.COMMUNITY_BASE_URL -import kotlinx.coroutines.* -import okhttp3.OkHttpClient -import okhttp3.Request +import com.dscvit.vitty.network.api.community.APICommunityRestClient +import com.dscvit.vitty.network.api.community.RetrofitServerStatusListener +import retrofit2.Call import timber.log.Timber -import java.util.concurrent.TimeUnit object MaintenanceChecker { - - private const val MAINTENANCE_CHECK_TIMEOUT = 5L + private const val API_OPERATIONAL_MESSAGE = "Welcome to VITTY API!🎉" - private var isChecking = false - + fun checkMaintenanceStatusAsync( context: Context, onResult: (isUnderMaintenance: Boolean) -> Unit ) { if (isChecking) return - + isChecking = true - - CoroutineScope(Dispatchers.IO).launch { - val isUnderMaintenance = try { - val client = OkHttpClient.Builder() - .connectTimeout(MAINTENANCE_CHECK_TIMEOUT, TimeUnit.SECONDS) - .readTimeout(MAINTENANCE_CHECK_TIMEOUT, TimeUnit.SECONDS) - .callTimeout(MAINTENANCE_CHECK_TIMEOUT, TimeUnit.SECONDS) - .build() - - val request = Request.Builder() - .url(COMMUNITY_BASE_URL) - .build() - - val response = client.newCall(request).execute() - val responseBody = response.body?.string() - - val result = when { - !response.isSuccessful -> { - Timber.d("Server returned error code: ${response.code}") - true // Server error = maintenance - } - responseBody == null -> { - Timber.d("No response body received") - true // No response = maintenance - } - responseBody.contains(API_OPERATIONAL_MESSAGE) -> { - Timber.d("API operational message found") - false // API working - } - else -> { - Timber.d("Different response received: $responseBody") - true // Different response = maintenance + + APICommunityRestClient.instance.checkServerStatus( + object : RetrofitServerStatusListener { + override fun onSuccess(call: Call, response: String?, isSuccessful: Boolean) { + val result = when { + !isSuccessful -> { + Timber.d("Server returned error") + true // Server error = maintenance + } + response == null -> { + Timber.d("No response body received") + true // No response = maintenance + } + response.contains(API_OPERATIONAL_MESSAGE) -> { + Timber.d("API operational message found") + false // API working + } + else -> { + Timber.d("Different response received: $response") + true // Different response = maintenance + } } + + Timber.d("Maintenance check: isUnderMaintenance=$result, response=$response") + isChecking = false + onResult(result) + } + + override fun onError(call: Call, t: Throwable) { + Timber.e(t, "Maintenance check failed") + isChecking = false + onResult(false) // Network error = don't assume maintenance } - - Timber.d("Maintenance check: isUnderMaintenance=$result, response=$responseBody") - result - - } catch (e: Exception) { - Timber.e(e, "Maintenance check failed") - false // Network error = don't assume maintenance, just fail silently - } - - withContext(Dispatchers.Main) { - isChecking = false - onResult(isUnderMaintenance) } - } - } - - fun isNetworkAvailable(context: Context): Boolean { - return UtilFunctions.isNetworkAvailable(context) + ) } -} +} \ No newline at end of file From 9050224a787c0c7e939fc223514a88d12af531cc Mon Sep 17 00:00:00 2001 From: Dev Keshwani Date: Tue, 5 Aug 2025 11:19:13 +0530 Subject: [PATCH 4/4] fix: add backend time string parsing utility and update timestamp handling in TodayWidget --- app/build.gradle | 2 +- .../com/dscvit/vitty/util/UtilFunctions.kt | 29 +++++++++++++++++++ .../com/dscvit/vitty/widget/TodayWidget.kt | 22 +++----------- 3 files changed, 34 insertions(+), 19 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 47e6dca..20d3f6d 100755 --- a/app/build.gradle +++ b/app/build.gradle @@ -35,7 +35,7 @@ android { namespace "com.dscvit.vitty" minSdkVersion 26 targetSdkVersion 36 - versionCode 43 + versionCode 44 versionName "3.0.1" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" signingConfig signingConfigs.release diff --git a/app/src/main/java/com/dscvit/vitty/util/UtilFunctions.kt b/app/src/main/java/com/dscvit/vitty/util/UtilFunctions.kt index eaab2ef..94fbd41 100755 --- a/app/src/main/java/com/dscvit/vitty/util/UtilFunctions.kt +++ b/app/src/main/java/com/dscvit/vitty/util/UtilFunctions.kt @@ -23,6 +23,7 @@ import java.text.SimpleDateFormat import java.util.Calendar import java.util.Date import java.util.Locale +import java.util.TimeZone object UtilFunctions { fun openLink( @@ -144,4 +145,32 @@ object UtilFunctions { intent.putExtra(Intent.EXTRA_STREAM, bitmapUri) context.startActivity(Intent.createChooser(intent, "Share")) } + + fun parseBackendTimeString(timeString: String): Date? { + try { + val timeRegex = """T(\d{2}):(\d{2}):(\d{2})""".toRegex() + val match = timeRegex.find(timeString) + + if (match != null) { + val hours = match.groupValues[1].toInt() + val minutes = match.groupValues[2].toInt() + val seconds = match.groupValues[3].toInt() + + val calendar = Calendar.getInstance() + calendar.set(Calendar.HOUR_OF_DAY, hours) + calendar.set(Calendar.MINUTE, minutes) + calendar.set(Calendar.SECOND, seconds) + calendar.set(Calendar.MILLISECOND, 0) + + return calendar.time + } else { + val sdf = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.Builder().setLanguage("en").setRegion("IN").build()) + sdf.timeZone = TimeZone.getTimeZone("Asia/Kolkata") + return sdf.parse(timeString) + } + } catch (e: Exception) { + val calendar = Calendar.getInstance(TimeZone.getTimeZone("Asia/Kolkata")) + return calendar.time + } + } } diff --git a/app/src/main/java/com/dscvit/vitty/widget/TodayWidget.kt b/app/src/main/java/com/dscvit/vitty/widget/TodayWidget.kt index 218a58b..844214d 100755 --- a/app/src/main/java/com/dscvit/vitty/widget/TodayWidget.kt +++ b/app/src/main/java/com/dscvit/vitty/widget/TodayWidget.kt @@ -306,7 +306,7 @@ suspend fun fetchTodayData( var endTime = parseTimeToTimestamp(period.end_time).toDate() val simpleDateFormat = - SimpleDateFormat("h:mm a", Locale.getDefault()) + SimpleDateFormat("h:mm a", Locale("en", "IN")) val sTime: String = simpleDateFormat.format(startTime).uppercase(Locale.ROOT) val eTime: String = @@ -347,20 +347,11 @@ suspend fun fetchTodayData( fun parseTimeToTimestamp(timeString: String): Timestamp = try { - val sanitizedTime = - if (timeString.contains("+05:53")) { - timeString.replace("+05:53", "+05:30") - } else { - timeString - } - val time = replaceYearIfZero(sanitizedTime) - - val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX", Locale.getDefault()) - val date = dateFormat.parse(time) + val date = UtilFunctions.parseBackendTimeString(timeString) if (date != null) { Timestamp(date) } else { - Timber.d("Date parsing error: Unable to parse sanitized time: $time") + Timber.d("Date parsing error: Unable to parse time: $timeString") Timestamp.now() } } catch (e: Exception) { @@ -368,9 +359,4 @@ fun parseTimeToTimestamp(timeString: String): Timestamp = Timestamp.now() } -fun replaceYearIfZero(dateStr: String): String = - if (dateStr.startsWith("0")) { - "2023" + dateStr.substring(4) - } else { - dateStr - } +