From 327dfba42526b479521550fe234d86ee7743c2a8 Mon Sep 17 00:00:00 2001 From: Atridad Lahiji Date: Fri, 22 Aug 2025 19:11:21 -0600 Subject: [PATCH] 1.0.1 - Notification reliability update --- app/build.gradle.kts | 4 +- app/src/main/AndroidManifest.xml | 1 + .../service/SessionTrackingService.kt | 134 ++++++++++++++++-- .../com/atridad/openclimb/ui/OpenClimbApp.kt | 63 +++++++- .../NotificationPermissionDialog.kt | 92 ++++++++++++ .../openclimb/ui/viewmodel/ClimbViewModel.kt | 31 ++++ .../utils/NotificationPermissionUtils.kt | 39 +++++ 7 files changed, 346 insertions(+), 18 deletions(-) create mode 100644 app/src/main/java/com/atridad/openclimb/ui/components/NotificationPermissionDialog.kt create mode 100644 app/src/main/java/com/atridad/openclimb/utils/NotificationPermissionUtils.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a5c9cce..7f2b836 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -14,8 +14,8 @@ android { applicationId = "com.atridad.openclimb" minSdk = 31 targetSdk = 36 - versionCode = 14 - versionName = "1.0.0" + versionCode = 15 + versionName = "1.0.1" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 52cee07..9157e08 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -12,6 +12,7 @@ + + // Handle permission result + if (isGranted) { + // Permission granted, continue + } else { + // Permission denied, show dialog again later + showNotificationPermissionDialog = false + } + } + + // Check notification permission on first launch + LaunchedEffect(Unit) { + if (!hasCheckedNotificationPermission) { + hasCheckedNotificationPermission = true + + if (NotificationPermissionUtils.shouldRequestNotificationPermission() && + !NotificationPermissionUtils.isNotificationPermissionGranted(context)) { + showNotificationPermissionDialog = true + } + } + } + + // Ensure session tracking service is running when app resumes + LaunchedEffect(Unit) { + viewModel.ensureSessionTrackingServiceRunning(context) + } + // FAB configuration var fabConfig by remember { mutableStateOf(null) } @@ -71,10 +112,16 @@ fun OpenClimbApp() { icon = Icons.Default.PlayArrow, contentDescription = "Start Session", onClick = { - if (gyms.size == 1) { - viewModel.startSession(context, gyms.first().id) + // Check notification permission before starting session + if (NotificationPermissionUtils.shouldRequestNotificationPermission() && + !NotificationPermissionUtils.isNotificationPermissionGranted(context)) { + showNotificationPermissionDialog = true } else { - navController.navigate(Screen.AddEditSession()) + if (gyms.size == 1) { + viewModel.startSession(context, gyms.first().id) + } else { + navController.navigate(Screen.AddEditSession()) + } } } ) @@ -224,6 +271,16 @@ fun OpenClimbApp() { ) } } + + // Notification permission dialog + if (showNotificationPermissionDialog) { + NotificationPermissionDialog( + onDismiss = { showNotificationPermissionDialog = false }, + onRequestPermission = { + permissionLauncher.launch(NotificationPermissionUtils.getNotificationPermissionString()) + } + ) + } } } diff --git a/app/src/main/java/com/atridad/openclimb/ui/components/NotificationPermissionDialog.kt b/app/src/main/java/com/atridad/openclimb/ui/components/NotificationPermissionDialog.kt new file mode 100644 index 0000000..020daa7 --- /dev/null +++ b/app/src/main/java/com/atridad/openclimb/ui/components/NotificationPermissionDialog.kt @@ -0,0 +1,92 @@ +package com.atridad.openclimb.ui.components + +import androidx.compose.foundation.layout.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Notifications +import androidx.compose.material3.* +import androidx.compose.runtime.* +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.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import com.atridad.openclimb.utils.NotificationPermissionUtils + +@Composable +fun NotificationPermissionDialog( + onDismiss: () -> Unit, + onRequestPermission: () -> Unit +) { + val context = LocalContext.current + + Dialog( + onDismissRequest = onDismiss, + properties = DialogProperties( + dismissOnBackPress = false, + dismissOnClickOutside = false + ) + ) { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + shape = MaterialTheme.shapes.medium + ) { + Column( + modifier = Modifier.padding(24.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Icon( + imageVector = Icons.Default.Notifications, + contentDescription = "Notifications", + modifier = Modifier.size(48.dp), + tint = MaterialTheme.colorScheme.primary + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = "Enable Notifications", + style = MaterialTheme.typography.headlineSmall, + fontWeight = MaterialTheme.typography.headlineSmall.fontWeight, + textAlign = TextAlign.Center + ) + + Spacer(modifier = Modifier.height(12.dp)) + + Text( + text = "OpenClimb needs notification permission to show your active climbing session. This helps you track your progress and ensures the session doesn't get interrupted.", + style = MaterialTheme.typography.bodyMedium, + textAlign = TextAlign.Center, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + + Spacer(modifier = Modifier.height(24.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + TextButton( + onClick = onDismiss, + modifier = Modifier.weight(1f) + ) { + Text("Not Now") + } + + Button( + onClick = { + onRequestPermission() + onDismiss() + }, + modifier = Modifier.weight(1f) + ) { + Text("Enable") + } + } + } + } + } +} diff --git a/app/src/main/java/com/atridad/openclimb/ui/viewmodel/ClimbViewModel.kt b/app/src/main/java/com/atridad/openclimb/ui/viewmodel/ClimbViewModel.kt index 9c2281d..01c9cfb 100644 --- a/app/src/main/java/com/atridad/openclimb/ui/viewmodel/ClimbViewModel.kt +++ b/app/src/main/java/com/atridad/openclimb/ui/viewmodel/ClimbViewModel.kt @@ -147,6 +147,14 @@ class ClimbViewModel( // Active session management fun startSession(context: Context, gymId: String, notes: String? = null) { viewModelScope.launch { + // Check notification permission first + if (!com.atridad.openclimb.utils.NotificationPermissionUtils.isNotificationPermissionGranted(context)) { + _uiState.value = _uiState.value.copy( + error = "Notification permission is required to track your climbing session. Please enable notifications in settings." + ) + return@launch + } + val existingActive = repository.getActiveSession() if (existingActive != null) { _uiState.value = _uiState.value.copy( @@ -170,6 +178,14 @@ class ClimbViewModel( fun endSession(context: Context, sessionId: String) { viewModelScope.launch { + // Check notification permission first + if (!com.atridad.openclimb.utils.NotificationPermissionUtils.isNotificationPermissionGranted(context)) { + _uiState.value = _uiState.value.copy( + error = "Notification permission is required to manage your climbing session. Please enable notifications in settings." + ) + return@launch + } + val session = repository.getSessionById(sessionId) if (session != null && session.status == SessionStatus.ACTIVE) { val completedSession = with(ClimbSession) { session.complete() } @@ -186,6 +202,21 @@ class ClimbViewModel( } } + /** + * Check if the session tracking service is running and restart it if needed + */ + fun ensureSessionTrackingServiceRunning(context: Context) { + viewModelScope.launch { + val activeSession = repository.getActiveSession() + if (activeSession != null && activeSession.status == SessionStatus.ACTIVE) { + // Check if service is running by trying to start it again + // The service will handle duplicate starts gracefully + val serviceIntent = SessionTrackingService.createStartIntent(context, activeSession.id) + context.startForegroundService(serviceIntent) + } + } + } + // Attempt operations fun addAttempt(attempt: Attempt) { viewModelScope.launch { diff --git a/app/src/main/java/com/atridad/openclimb/utils/NotificationPermissionUtils.kt b/app/src/main/java/com/atridad/openclimb/utils/NotificationPermissionUtils.kt new file mode 100644 index 0000000..57f47a4 --- /dev/null +++ b/app/src/main/java/com/atridad/openclimb/utils/NotificationPermissionUtils.kt @@ -0,0 +1,39 @@ +package com.atridad.openclimb.utils + +import android.Manifest +import android.content.Context +import android.content.pm.PackageManager +import android.os.Build +import androidx.core.content.ContextCompat + +object NotificationPermissionUtils { + + /** + * Check if notification permission is granted + */ + fun isNotificationPermissionGranted(context: Context): Boolean { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + ContextCompat.checkSelfPermission( + context, + Manifest.permission.POST_NOTIFICATIONS + ) == PackageManager.PERMISSION_GRANTED + } else { + // For older versions, assume permission is granted + true + } + } + + /** + * Check if notification permission should be requested + */ + fun shouldRequestNotificationPermission(): Boolean { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU + } + + /** + * Get the notification permission string + */ + fun getNotificationPermissionString(): String { + return Manifest.permission.POST_NOTIFICATIONS + } +}