diff --git a/.gitignore b/.gitignore index f124454..8a08472 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ local.properties # Log/OS Files *.log +.DS_Store # Android Studio generated files and folders captures/ @@ -32,4 +33,4 @@ render.experimental.xml google-services.json # Android Profiling -*.hprof \ No newline at end of file +*.hprof diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 567f39b..072651c 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -16,8 +16,8 @@ android { applicationId = "com.atridad.openclimb" minSdk = 34 targetSdk = 36 - versionCode = 21 - versionName = "1.4.0" + versionCode = 22 + versionName = "1.4.1" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } diff --git a/app/src/main/java/com/atridad/openclimb/MainActivity.kt b/app/src/main/java/com/atridad/openclimb/MainActivity.kt index 827ea59..928a51f 100644 --- a/app/src/main/java/com/atridad/openclimb/MainActivity.kt +++ b/app/src/main/java/com/atridad/openclimb/MainActivity.kt @@ -14,6 +14,12 @@ import com.atridad.openclimb.ui.theme.OpenClimbTheme class MainActivity : ComponentActivity() { private var shortcutAction by mutableStateOf(null) + private var lastUsedGymId by mutableStateOf(null) + + fun clearShortcutAction() { + shortcutAction = null + lastUsedGymId = null + } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -21,11 +27,16 @@ class MainActivity : ComponentActivity() { enableEdgeToEdge() shortcutAction = intent?.action + lastUsedGymId = intent?.getStringExtra("LAST_USED_GYM_ID") setContent { OpenClimbTheme { Surface(modifier = Modifier.fillMaxSize()) { - OpenClimbApp(shortcutAction = shortcutAction) + OpenClimbApp( + shortcutAction = shortcutAction, + lastUsedGymId = lastUsedGymId, + onShortcutActionProcessed = { clearShortcutAction() } + ) } } } @@ -36,5 +47,6 @@ class MainActivity : ComponentActivity() { setIntent(intent) shortcutAction = intent.action + lastUsedGymId = intent.getStringExtra("LAST_USED_GYM_ID") } } diff --git a/app/src/main/java/com/atridad/openclimb/data/repository/ClimbRepository.kt b/app/src/main/java/com/atridad/openclimb/data/repository/ClimbRepository.kt index 4a10d5c..0670e30 100644 --- a/app/src/main/java/com/atridad/openclimb/data/repository/ClimbRepository.kt +++ b/app/src/main/java/com/atridad/openclimb/data/repository/ClimbRepository.kt @@ -1,32 +1,26 @@ package com.atridad.openclimb.data.repository import android.content.Context -import android.os.Environment import com.atridad.openclimb.data.database.OpenClimbDatabase import com.atridad.openclimb.data.model.* import com.atridad.openclimb.utils.ZipExportImportUtils -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.first -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json import java.io.File import java.time.LocalDateTime +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.first +import kotlinx.serialization.json.Json -class ClimbRepository( - database: OpenClimbDatabase, - private val context: Context -) { +class ClimbRepository(database: OpenClimbDatabase, private val context: Context) { private val gymDao = database.gymDao() private val problemDao = database.problemDao() private val sessionDao = database.climbSessionDao() private val attemptDao = database.attemptDao() - - private val json = Json { - prettyPrint = true + private val json = Json { + prettyPrint = true ignoreUnknownKeys = true } - + // Gym operations fun getAllGyms(): Flow> = gymDao.getAllGyms() suspend fun getGymById(id: String): Gym? = gymDao.getGymById(id) @@ -34,7 +28,7 @@ class ClimbRepository( suspend fun updateGym(gym: Gym) = gymDao.updateGym(gym) suspend fun deleteGym(gym: Gym) = gymDao.deleteGym(gym) fun searchGyms(query: String): Flow> = gymDao.searchGyms(query) - + // Problem operations fun getAllProblems(): Flow> = problemDao.getAllProblems() suspend fun getProblemById(id: String): Problem? = problemDao.getProblemById(id) @@ -43,27 +37,36 @@ class ClimbRepository( suspend fun updateProblem(problem: Problem) = problemDao.updateProblem(problem) suspend fun deleteProblem(problem: Problem) = problemDao.deleteProblem(problem) fun searchProblems(query: String): Flow> = problemDao.searchProblems(query) - + // Session operations fun getAllSessions(): Flow> = sessionDao.getAllSessions() suspend fun getSessionById(id: String): ClimbSession? = sessionDao.getSessionById(id) - fun getSessionsByGym(gymId: String): Flow> = sessionDao.getSessionsByGym(gymId) + fun getSessionsByGym(gymId: String): Flow> = + sessionDao.getSessionsByGym(gymId) suspend fun getActiveSession(): ClimbSession? = sessionDao.getActiveSession() fun getActiveSessionFlow(): Flow = sessionDao.getActiveSessionFlow() suspend fun insertSession(session: ClimbSession) = sessionDao.insertSession(session) suspend fun updateSession(session: ClimbSession) = sessionDao.updateSession(session) suspend fun deleteSession(session: ClimbSession) = sessionDao.deleteSession(session) - + suspend fun getLastUsedGym(): Gym? { + val recentSessions = sessionDao.getRecentSessions(1).first() + return if (recentSessions.isNotEmpty()) { + getGymById(recentSessions.first().gymId) + } else { + null + } + } + // Attempt operations fun getAllAttempts(): Flow> = attemptDao.getAllAttempts() - fun getAttemptsBySession(sessionId: String): Flow> = attemptDao.getAttemptsBySession(sessionId) - fun getAttemptsByProblem(problemId: String): Flow> = attemptDao.getAttemptsByProblem(problemId) + fun getAttemptsBySession(sessionId: String): Flow> = + attemptDao.getAttemptsBySession(sessionId) + fun getAttemptsByProblem(problemId: String): Flow> = + attemptDao.getAttemptsByProblem(problemId) suspend fun insertAttempt(attempt: Attempt) = attemptDao.insertAttempt(attempt) suspend fun updateAttempt(attempt: Attempt) = attemptDao.updateAttempt(attempt) suspend fun deleteAttempt(attempt: Attempt) = attemptDao.deleteAttempt(attempt) - - // ZIP Export with images - Single format for reliability suspend fun exportAllDataToZip(directory: File? = null): File { try { @@ -72,47 +75,58 @@ class ClimbRepository( val allProblems = problemDao.getAllProblems().first() val allSessions = sessionDao.getAllSessions().first() val allAttempts = attemptDao.getAllAttempts().first() - + // Validate data integrity before export validateDataIntegrity(allGyms, allProblems, allSessions, allAttempts) - - val exportData = ClimbDataExport( - exportedAt = LocalDateTime.now().toString(), - version = "1.0", - gyms = allGyms, - problems = allProblems, - sessions = allSessions, - attempts = allAttempts - ) - + + val exportData = + ClimbDataExport( + exportedAt = LocalDateTime.now().toString(), + version = "1.0", + gyms = allGyms, + problems = allProblems, + sessions = allSessions, + attempts = allAttempts + ) + // Collect all referenced image paths and validate they exist val referencedImagePaths = allProblems.flatMap { it.imagePaths }.toSet() - val validImagePaths = referencedImagePaths.filter { imagePath -> - try { - val imageFile = com.atridad.openclimb.utils.ImageUtils.getImageFile(context, imagePath) - imageFile.exists() && imageFile.length() > 0 - } catch (e: Exception) { - false - } - }.toSet() - + val validImagePaths = + referencedImagePaths + .filter { imagePath -> + try { + val imageFile = + com.atridad.openclimb.utils.ImageUtils.getImageFile( + context, + imagePath + ) + imageFile.exists() && imageFile.length() > 0 + } catch (e: Exception) { + false + } + } + .toSet() + // Log any missing images for debugging val missingImages = referencedImagePaths - validImagePaths if (missingImages.isNotEmpty()) { - android.util.Log.w("ClimbRepository", "Some referenced images are missing: $missingImages") + android.util.Log.w( + "ClimbRepository", + "Some referenced images are missing: $missingImages" + ) } - + return ZipExportImportUtils.createExportZip( - context = context, - exportData = exportData, - referencedImagePaths = validImagePaths, - directory = directory + context = context, + exportData = exportData, + referencedImagePaths = validImagePaths, + directory = directory ) } catch (e: Exception) { throw Exception("Export failed: ${e.message}") } } - + suspend fun exportAllDataToZipUri(context: Context, uri: android.net.Uri) { try { // Collect all data with proper error handling @@ -120,72 +134,81 @@ class ClimbRepository( val allProblems = problemDao.getAllProblems().first() val allSessions = sessionDao.getAllSessions().first() val allAttempts = attemptDao.getAllAttempts().first() - + // Validate data integrity before export validateDataIntegrity(allGyms, allProblems, allSessions, allAttempts) - - val exportData = ClimbDataExport( - exportedAt = LocalDateTime.now().toString(), - version = "1.0", - gyms = allGyms, - problems = allProblems, - sessions = allSessions, - attempts = allAttempts - ) - + + val exportData = + ClimbDataExport( + exportedAt = LocalDateTime.now().toString(), + version = "1.0", + gyms = allGyms, + problems = allProblems, + sessions = allSessions, + attempts = allAttempts + ) + // Collect all referenced image paths and validate they exist val referencedImagePaths = allProblems.flatMap { it.imagePaths }.toSet() - val validImagePaths = referencedImagePaths.filter { imagePath -> - try { - val imageFile = com.atridad.openclimb.utils.ImageUtils.getImageFile(context, imagePath) - imageFile.exists() && imageFile.length() > 0 - } catch (e: Exception) { - false - } - }.toSet() - + val validImagePaths = + referencedImagePaths + .filter { imagePath -> + try { + val imageFile = + com.atridad.openclimb.utils.ImageUtils.getImageFile( + context, + imagePath + ) + imageFile.exists() && imageFile.length() > 0 + } catch (e: Exception) { + false + } + } + .toSet() + ZipExportImportUtils.createExportZipToUri( - context = context, - uri = uri, - exportData = exportData, - referencedImagePaths = validImagePaths + context = context, + uri = uri, + exportData = exportData, + referencedImagePaths = validImagePaths ) } catch (e: Exception) { throw Exception("Export failed: ${e.message}") } } - + suspend fun importDataFromZip(file: File) { try { // Validate the ZIP file if (!file.exists() || file.length() == 0L) { throw Exception("Invalid ZIP file: file is empty or doesn't exist") } - + // Extract and validate the ZIP contents val importResult = ZipExportImportUtils.extractImportZip(context, file) - + // Validate JSON content if (importResult.jsonContent.isBlank()) { throw Exception("Invalid ZIP file: no data.json found or empty content") } - + // Parse and validate the data structure - val importData = try { - json.decodeFromString(importResult.jsonContent) - } catch (e: Exception) { - throw Exception("Invalid data format: ${e.message}") - } - + val importData = + try { + json.decodeFromString(importResult.jsonContent) + } catch (e: Exception) { + throw Exception("Invalid data format: ${e.message}") + } + // Validate data integrity validateImportData(importData) - + // Clear existing data to avoid conflicts attemptDao.deleteAllAttempts() sessionDao.deleteAllSessions() problemDao.deleteAllProblems() gymDao.deleteAllGyms() - + // Import gyms first (problems depend on gyms) importData.gyms.forEach { gym -> try { @@ -194,13 +217,14 @@ class ClimbRepository( throw Exception("Failed to import gym ${gym.name}: ${e.message}") } } - + // Import problems with updated image paths - val updatedProblems = ZipExportImportUtils.updateProblemImagePaths( - importData.problems, - importResult.importedImagePaths - ) - + val updatedProblems = + ZipExportImportUtils.updateProblemImagePaths( + importData.problems, + importResult.importedImagePaths + ) + updatedProblems.forEach { problem -> try { problemDao.insertProblem(problem) @@ -208,7 +232,7 @@ class ClimbRepository( throw Exception("Failed to import problem ${problem.name}: ${e.message}") } } - + // Import sessions importData.sessions.forEach { session -> try { @@ -217,7 +241,7 @@ class ClimbRepository( throw Exception("Failed to import session: ${e.message}") } } - + // Import attempts last (depends on problems and sessions) importData.attempts.forEach { attempt -> try { @@ -226,61 +250,66 @@ class ClimbRepository( throw Exception("Failed to import attempt: ${e.message}") } } - } catch (e: Exception) { throw Exception("Import failed: ${e.message}") } } - + private fun validateDataIntegrity( - gyms: List, - problems: List, - sessions: List, - attempts: List + gyms: List, + problems: List, + sessions: List, + attempts: List ) { // Validate that all problems reference valid gyms val gymIds = gyms.map { it.id }.toSet() val invalidProblems = problems.filter { it.gymId !in gymIds } if (invalidProblems.isNotEmpty()) { - throw Exception("Data integrity error: ${invalidProblems.size} problems reference non-existent gyms") + throw Exception( + "Data integrity error: ${invalidProblems.size} problems reference non-existent gyms" + ) } - + // Validate that all sessions reference valid gyms val invalidSessions = sessions.filter { it.gymId !in gymIds } if (invalidSessions.isNotEmpty()) { - throw Exception("Data integrity error: ${invalidSessions.size} sessions reference non-existent gyms") + throw Exception( + "Data integrity error: ${invalidSessions.size} sessions reference non-existent gyms" + ) } - + // Validate that all attempts reference valid problems and sessions val problemIds = problems.map { it.id }.toSet() val sessionIds = sessions.map { it.id }.toSet() - - val invalidAttempts = attempts.filter { - it.problemId !in problemIds || it.sessionId !in sessionIds - } + + val invalidAttempts = + attempts.filter { it.problemId !in problemIds || it.sessionId !in sessionIds } if (invalidAttempts.isNotEmpty()) { - throw Exception("Data integrity error: ${invalidAttempts.size} attempts reference non-existent problems or sessions") + throw Exception( + "Data integrity error: ${invalidAttempts.size} attempts reference non-existent problems or sessions" + ) } } - + private fun validateImportData(importData: ClimbDataExport) { if (importData.gyms.isEmpty()) { throw Exception("Import data is invalid: no gyms found") } - + if (importData.version.isBlank()) { throw Exception("Import data is invalid: no version information") } - + // Check for reasonable data sizes to prevent malicious imports - if (importData.gyms.size > 1000 || - importData.problems.size > 10000 || - importData.sessions.size > 10000 || - importData.attempts.size > 100000) { + if (importData.gyms.size > 1000 || + importData.problems.size > 10000 || + importData.sessions.size > 10000 || + importData.attempts.size > 100000 + ) { throw Exception("Import data is too large: possible corruption or malicious file") } } - + suspend fun resetAllData() { try { // Clear all data from database @@ -288,15 +317,14 @@ class ClimbRepository( sessionDao.deleteAllSessions() problemDao.deleteAllProblems() gymDao.deleteAllGyms() - + // Clear all images from storage clearAllImages() - } catch (e: Exception) { throw Exception("Reset failed: ${e.message}") } } - + private fun clearAllImages() { try { // Get the images directory @@ -314,10 +342,10 @@ class ClimbRepository( @kotlinx.serialization.Serializable data class ClimbDataExport( - val exportedAt: String, - val version: String = "1.0", - val gyms: List, - val problems: List, - val sessions: List, - val attempts: List -) \ No newline at end of file + val exportedAt: String, + val version: String = "1.0", + val gyms: List, + val problems: List, + val sessions: List, + val attempts: List +) diff --git a/app/src/main/java/com/atridad/openclimb/navigation/BottomNavigationItem.kt b/app/src/main/java/com/atridad/openclimb/navigation/BottomNavigationItem.kt index bd22164..a966854 100644 --- a/app/src/main/java/com/atridad/openclimb/navigation/BottomNavigationItem.kt +++ b/app/src/main/java/com/atridad/openclimb/navigation/BottomNavigationItem.kt @@ -4,36 +4,33 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.* import androidx.compose.ui.graphics.vector.ImageVector -data class BottomNavigationItem( - val screen: Screen, - val icon: ImageVector, - val label: String -) +data class BottomNavigationItem(val screen: Screen, val icon: ImageVector, val label: String) -val bottomNavigationItems = listOf( - BottomNavigationItem( - screen = Screen.Sessions, - icon = Icons.Default.PlayArrow, - label = "Sessions" - ), - BottomNavigationItem( - screen = Screen.Problems, - icon = Icons.Default.Star, - label = "Problems" - ), - BottomNavigationItem( - screen = Screen.Analytics, - icon = Icons.Default.Info, - label = "Analytics" - ), - BottomNavigationItem( - screen = Screen.Gyms, - icon = Icons.Default.LocationOn, - label = "Gyms" - ), - BottomNavigationItem( - screen = Screen.Settings, - icon = Icons.Default.Settings, - label = "Settings" - ) -) +val bottomNavigationItems = + listOf( + BottomNavigationItem( + screen = Screen.Sessions, + icon = Icons.Default.PlayArrow, + label = "Sessions" + ), + BottomNavigationItem( + screen = Screen.Problems, + icon = Icons.Default.Star, + label = "Problems" + ), + BottomNavigationItem( + screen = Screen.Analytics, + icon = Icons.Default.Info, + label = "Analytics" + ), + BottomNavigationItem( + screen = Screen.Gyms, + icon = Icons.Default.LocationOn, + label = "Gyms" + ), + BottomNavigationItem( + screen = Screen.Settings, + icon = Icons.Default.Settings, + label = "Settings" + ) + ) diff --git a/app/src/main/java/com/atridad/openclimb/ui/OpenClimbApp.kt b/app/src/main/java/com/atridad/openclimb/ui/OpenClimbApp.kt index aae5cf1..83ec388 100644 --- a/app/src/main/java/com/atridad/openclimb/ui/OpenClimbApp.kt +++ b/app/src/main/java/com/atridad/openclimb/ui/OpenClimbApp.kt @@ -11,6 +11,7 @@ import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavController import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable @@ -30,10 +31,16 @@ import com.atridad.openclimb.utils.NotificationPermissionUtils @OptIn(ExperimentalMaterial3Api::class) @Composable -fun OpenClimbApp(shortcutAction: String? = null) { +fun OpenClimbApp( + shortcutAction: String? = null, + lastUsedGymId: String? = null, + onShortcutActionProcessed: () -> Unit = {} +) { val navController = rememberNavController() val context = LocalContext.current + var lastUsedGym by remember { mutableStateOf(null) } + val database = remember { OpenClimbDatabase.getDatabase(context) } val repository = remember { ClimbRepository(database, context) } val viewModel: ClimbViewModel = viewModel(factory = ClimbViewModelFactory(repository)) @@ -69,11 +76,19 @@ fun OpenClimbApp(shortcutAction: String? = null) { val activeSession by viewModel.activeSession.collectAsState() val gyms by viewModel.gyms.collectAsState() - LaunchedEffect(activeSession, gyms) { + // Update last used gym when gyms change + LaunchedEffect(gyms) { + if (gyms.isNotEmpty() && lastUsedGym == null) { + lastUsedGym = viewModel.getLastUsedGym() + } + } + + LaunchedEffect(activeSession, gyms, lastUsedGym) { AppShortcutManager.updateShortcuts( context = context, hasActiveSession = activeSession != null, - hasGyms = gyms.isNotEmpty() + hasGyms = gyms.isNotEmpty(), + lastUsedGym = if (activeSession == null && gyms.size > 1) lastUsedGym else null ) } @@ -84,22 +99,6 @@ fun OpenClimbApp(shortcutAction: String? = null) { popUpTo(0) { inclusive = true } launchSingleTop = true } - - if (activeSession == null && gyms.isNotEmpty()) { - if (NotificationPermissionUtils.shouldRequestNotificationPermission() && - !NotificationPermissionUtils.isNotificationPermissionGranted( - context - ) - ) { - showNotificationPermissionDialog = true - } else { - if (gyms.size == 1) { - viewModel.startSession(context, gyms.first().id) - } else { - navController.navigate(Screen.AddEditSession()) - } - } - } } AppShortcutManager.ACTION_END_SESSION -> { navController.navigate(Screen.Sessions) { @@ -112,6 +111,62 @@ fun OpenClimbApp(shortcutAction: String? = null) { } } + // Process shortcut actions after data is loaded + LaunchedEffect(shortcutAction, activeSession, gyms, lastUsedGym) { + if (shortcutAction == AppShortcutManager.ACTION_START_SESSION && gyms.isNotEmpty()) { + android.util.Log.d( + "OpenClimbApp", + "Processing shortcut action: activeSession=$activeSession, gyms.size=${gyms.size}, lastUsedGymId=$lastUsedGymId, lastUsedGym=${lastUsedGym?.name}" + ) + + if (activeSession == null) { + if (NotificationPermissionUtils.shouldRequestNotificationPermission() && + !NotificationPermissionUtils.isNotificationPermissionGranted( + context + ) + ) { + android.util.Log.d("OpenClimbApp", "Showing notification permission dialog") + showNotificationPermissionDialog = true + } else { + if (gyms.size == 1) { + android.util.Log.d( + "OpenClimbApp", + "Starting session with single gym: ${gyms.first().name}" + ) + viewModel.startSession(context, gyms.first().id) + } else { + // Try to get the last used gym from the intent or fallback to state + val targetGym = + lastUsedGymId?.let { gymId -> gyms.find { it.id == gymId } } + ?: lastUsedGym + + if (targetGym != null) { + android.util.Log.d( + "OpenClimbApp", + "Starting session with target gym: ${targetGym.name}" + ) + viewModel.startSession(context, targetGym.id) + } else { + android.util.Log.d( + "OpenClimbApp", + "No target gym found, navigating to selection" + ) + navController.navigate(Screen.AddEditSession()) + } + } + } + } else { + android.util.Log.d( + "OpenClimbApp", + "Active session already exists: ${activeSession?.id}" + ) + } + + // Clear the shortcut action after processing to prevent repeated execution + onShortcutActionProcessed() + } + } + var fabConfig by remember { mutableStateOf(null) } Scaffold( @@ -155,6 +210,8 @@ fun OpenClimbApp(shortcutAction: String? = null) { if (gyms.size == 1) { viewModel.startSession(context, gyms.first().id) } else { + // Always show gym selection for FAB when + // multiple gyms navController.navigate(Screen.AddEditSession()) } } @@ -173,7 +230,6 @@ fun OpenClimbApp(shortcutAction: String? = null) { } composable { - val gyms by viewModel.gyms.collectAsState() LaunchedEffect(gyms) { fabConfig = if (gyms.isNotEmpty()) { 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 6a55597..375935d 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 @@ -190,12 +190,18 @@ class ClimbViewModel(private val repository: ClimbRepository) : ViewModel() { fun getSessionsByGym(gymId: String): Flow> = repository.getSessionsByGym(gymId) + // Get last used gym for shortcut functionality + suspend fun getLastUsedGym(): Gym? = repository.getLastUsedGym() + // Active session management fun startSession(context: Context, gymId: String, notes: String? = null) { viewModelScope.launch { + android.util.Log.d("ClimbViewModel", "startSession called with gymId: $gymId") + if (!com.atridad.openclimb.utils.NotificationPermissionUtils .isNotificationPermissionGranted(context) ) { + android.util.Log.d("ClimbViewModel", "Notification permission not granted") _uiState.value = _uiState.value.copy( error = @@ -206,6 +212,10 @@ class ClimbViewModel(private val repository: ClimbRepository) : ViewModel() { val existingActive = repository.getActiveSession() if (existingActive != null) { + android.util.Log.d( + "ClimbViewModel", + "Active session already exists: ${existingActive.id}" + ) _uiState.value = _uiState.value.copy( error = "There's already an active session. Please end it first." @@ -213,15 +223,21 @@ class ClimbViewModel(private val repository: ClimbRepository) : ViewModel() { return@launch } + android.util.Log.d("ClimbViewModel", "Creating new session") val newSession = ClimbSession.create(gymId = gymId, notes = notes) repository.insertSession(newSession) + android.util.Log.d( + "ClimbViewModel", + "Starting tracking service for session: ${newSession.id}" + ) // Start the tracking service val serviceIntent = SessionTrackingService.createStartIntent(context, newSession.id) context.startForegroundService(serviceIntent) ClimbStatsWidgetProvider.updateAllWidgets(context) + android.util.Log.d("ClimbViewModel", "Session started successfully") _uiState.value = _uiState.value.copy(message = "Session started successfully!") } } diff --git a/app/src/main/java/com/atridad/openclimb/utils/ShortcutManager.kt b/app/src/main/java/com/atridad/openclimb/utils/ShortcutManager.kt index e9cf3ed..02052ac 100644 --- a/app/src/main/java/com/atridad/openclimb/utils/ShortcutManager.kt +++ b/app/src/main/java/com/atridad/openclimb/utils/ShortcutManager.kt @@ -19,7 +19,12 @@ object AppShortcutManager { const val ACTION_END_SESSION = "com.atridad.openclimb.action.END_SESSION" /** Updates the app shortcuts based on current session state */ - fun updateShortcuts(context: Context, hasActiveSession: Boolean, hasGyms: Boolean) { + fun updateShortcuts( + context: Context, + hasActiveSession: Boolean, + hasGyms: Boolean, + lastUsedGym: com.atridad.openclimb.data.model.Gym? = null + ) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { val shortcutManager = context.getSystemService(ShortcutManager::class.java) @@ -30,7 +35,7 @@ object AppShortcutManager { shortcuts.add(createEndSessionShortcut(context)) } else if (hasGyms) { // Show "Start Session" shortcut when no active session but gyms exist - shortcuts.add(createStartSessionShortcut(context)) + shortcuts.add(createStartSessionShortcut(context, lastUsedGym)) } shortcutManager.dynamicShortcuts = shortcuts @@ -38,16 +43,34 @@ object AppShortcutManager { } @RequiresApi(Build.VERSION_CODES.N_MR1) - private fun createStartSessionShortcut(context: Context): ShortcutInfo { + private fun createStartSessionShortcut( + context: Context, + lastUsedGym: com.atridad.openclimb.data.model.Gym? = null + ): ShortcutInfo { val startIntent = Intent(context, MainActivity::class.java).apply { action = ACTION_START_SESSION flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP + lastUsedGym?.let { gym -> putExtra("LAST_USED_GYM_ID", gym.id) } + } + + val shortLabel = + if (lastUsedGym != null) { + "Start at ${lastUsedGym.name}" + } else { + context.getString(R.string.shortcut_start_session_short) + } + + val longLabel = + if (lastUsedGym != null) { + "Start a new climbing session at ${lastUsedGym.name}" + } else { + context.getString(R.string.shortcut_start_session_long) } return ShortcutInfo.Builder(context, SHORTCUT_START_SESSION) - .setShortLabel(context.getString(R.string.shortcut_start_session_short)) - .setLongLabel(context.getString(R.string.shortcut_start_session_long)) + .setShortLabel(shortLabel) + .setLongLabel(longLabel) .setIcon(Icon.createWithResource(context, R.drawable.ic_play_arrow_24)) .setIntent(startIntent) .build()