diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index a5c9cce..f0563c7 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -12,10 +12,10 @@ android {
defaultConfig {
applicationId = "com.atridad.openclimb"
- minSdk = 31
+ minSdk = 33
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..9b463ea
--- /dev/null
+++ b/app/src/main/java/com/atridad/openclimb/ui/components/NotificationPermissionDialog.kt
@@ -0,0 +1,89 @@
+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
+
+@Composable
+fun NotificationPermissionDialog(
+ onDismiss: () -> Unit,
+ onRequestPermission: () -> Unit
+) {
+ 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..318dec5
--- /dev/null
+++ b/app/src/main/java/com/atridad/openclimb/utils/NotificationPermissionUtils.kt
@@ -0,0 +1,33 @@
+package com.atridad.openclimb.utils
+
+import android.Manifest
+import android.content.Context
+import android.content.pm.PackageManager
+import androidx.core.content.ContextCompat
+
+object NotificationPermissionUtils {
+
+ /**
+ * Check if notification permission is granted
+ */
+ fun isNotificationPermissionGranted(context: Context): Boolean {
+ return ContextCompat.checkSelfPermission(
+ context,
+ Manifest.permission.POST_NOTIFICATIONS
+ ) == PackageManager.PERMISSION_GRANTED
+ }
+
+ /**
+ * Check if notification permission should be requested
+ */
+ fun shouldRequestNotificationPermission(): Boolean {
+ return true
+ }
+
+ /**
+ * Get the notification permission string
+ */
+ fun getNotificationPermissionString(): String {
+ return Manifest.permission.POST_NOTIFICATIONS
+ }
+}