1.1.1 - More fixes for notification reliability

This commit is contained in:
2025-08-22 20:59:36 -06:00
parent 8d176592c4
commit 77e4df06f8
4 changed files with 50 additions and 75 deletions

View File

@@ -14,8 +14,8 @@ android {
applicationId = "com.atridad.openclimb" applicationId = "com.atridad.openclimb"
minSdk = 33 minSdk = 33
targetSdk = 36 targetSdk = 36
versionCode = 15 versionCode = 16
versionName = "1.1.0" versionName = "1.1.1"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
} }

View File

@@ -12,7 +12,6 @@
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<application <application
android:allowBackup="true" android:allowBackup="true"
@@ -52,7 +51,8 @@
android:name=".service.SessionTrackingService" android:name=".service.SessionTrackingService"
android:enabled="true" android:enabled="true"
android:exported="false" android:exported="false"
android:foregroundServiceType="specialUse"> android:foregroundServiceType="specialUse"
android:description="@string/session_tracking_service_description">
<meta-data <meta-data
android:name="android.app.foreground_service_type" android:name="android.app.foreground_service_type"
android:value="specialUse" /> android:value="specialUse" />

View File

@@ -7,7 +7,6 @@ import android.app.Service
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.IBinder import android.os.IBinder
import android.os.PowerManager
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import com.atridad.openclimb.MainActivity import com.atridad.openclimb.MainActivity
import com.atridad.openclimb.R import com.atridad.openclimb.R
@@ -17,13 +16,13 @@ import kotlinx.coroutines.*
import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.firstOrNull
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.temporal.ChronoUnit import java.time.temporal.ChronoUnit
import kotlinx.coroutines.runBlocking
class SessionTrackingService : Service() { class SessionTrackingService : Service() {
private val serviceScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) private val serviceScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private var notificationJob: Job? = null private var notificationJob: Job? = null
private var monitoringJob: Job? = null private var monitoringJob: Job? = null
private var wakeLock: PowerManager.WakeLock? = null
private lateinit var repository: ClimbRepository private lateinit var repository: ClimbRepository
private lateinit var notificationManager: NotificationManager private lateinit var notificationManager: NotificationManager
@@ -58,7 +57,6 @@ class SessionTrackingService : Service() {
notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
createNotificationChannel() createNotificationChannel()
acquireWakeLock()
} }
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
@@ -87,59 +85,51 @@ class SessionTrackingService : Service() {
} }
} }
} }
// Return START_STICKY to restart service if it gets killed
return START_STICKY return START_REDELIVER_INTENT
} }
override fun onTaskRemoved(rootIntent: Intent?) { override fun onTaskRemoved(rootIntent: Intent?) {
super.onTaskRemoved(rootIntent) super.onTaskRemoved(rootIntent)
// If the app is removed from recent tasks, ensure the service keeps running
// This helps maintain the notification even if the user swipes away the app
}
override fun onLowMemory() {
super.onLowMemory()
// Don't stop the service on low memory, just log it
// The notification is important for user experience
} }
override fun onBind(intent: Intent?): IBinder? = null override fun onBind(intent: Intent?): IBinder? = null
private fun startSessionTracking(sessionId: String) { private fun startSessionTracking(sessionId: String) {
// Cancel any existing jobs
notificationJob?.cancel() notificationJob?.cancel()
monitoringJob?.cancel() monitoringJob?.cancel()
// Start the main notification update job try {
createAndShowNotification(sessionId)
} catch (e: Exception) {
e.printStackTrace()
}
notificationJob = serviceScope.launch { notificationJob = serviceScope.launch {
try { try {
// Initial notification update if (!isNotificationActive()) {
updateNotification(sessionId) delay(1000L)
createAndShowNotification(sessionId)
}
// Update every 2 seconds for better performance
while (isActive) { while (isActive) {
delay(2000L) delay(5000L)
updateNotification(sessionId) updateNotification(sessionId)
} }
} catch (e: Exception) { } catch (e: Exception) {
// Log error and continue
e.printStackTrace() e.printStackTrace()
} }
} }
// Start the monitoring job that ensures notification stays active
monitoringJob = serviceScope.launch { monitoringJob = serviceScope.launch {
try { try {
while (isActive) { while (isActive) {
delay(5000L) // Check every 5 seconds delay(10000L)
// Verify the notification is still active
if (!isNotificationActive()) { if (!isNotificationActive()) {
// Notification was dismissed, recreate it
updateNotification(sessionId) updateNotification(sessionId)
} }
// Verify the session is still active
val session = repository.getSessionById(sessionId) val session = repository.getSessionById(sessionId)
if (session == null || session.status != com.atridad.openclimb.data.model.SessionStatus.ACTIVE) { if (session == null || session.status != com.atridad.openclimb.data.model.SessionStatus.ACTIVE) {
stopSessionTracking() stopSessionTracking()
@@ -155,7 +145,6 @@ class SessionTrackingService : Service() {
private fun stopSessionTracking() { private fun stopSessionTracking() {
notificationJob?.cancel() notificationJob?.cancel()
monitoringJob?.cancel() monitoringJob?.cancel()
releaseWakeLock()
stopForeground(STOP_FOREGROUND_REMOVE) stopForeground(STOP_FOREGROUND_REMOVE)
stopSelf() stopSelf()
} }
@@ -171,14 +160,37 @@ class SessionTrackingService : Service() {
private suspend fun updateNotification(sessionId: String) { private suspend fun updateNotification(sessionId: String) {
try { try {
val session = repository.getSessionById(sessionId) createAndShowNotification(sessionId)
} catch (e: Exception) {
e.printStackTrace()
try {
delay(10000L)
createAndShowNotification(sessionId)
} catch (retryException: Exception) {
retryException.printStackTrace()
stopSessionTracking()
}
}
}
private fun createAndShowNotification(sessionId: String) {
try {
val session = runBlocking {
repository.getSessionById(sessionId)
}
if (session == null || session.status != com.atridad.openclimb.data.model.SessionStatus.ACTIVE) { if (session == null || session.status != com.atridad.openclimb.data.model.SessionStatus.ACTIVE) {
stopSessionTracking() stopSessionTracking()
return return
} }
val gym = repository.getGymById(session.gymId) val gym = runBlocking {
val attempts = repository.getAttemptsBySession(sessionId).firstOrNull() ?: emptyList() repository.getGymById(session.gymId)
}
val attempts = runBlocking {
repository.getAttemptsBySession(sessionId).firstOrNull() ?: emptyList()
}
val duration = session.startTime?.let { startTime -> val duration = session.startTime?.let { startTime ->
try { try {
@@ -205,7 +217,7 @@ class SessionTrackingService : Service() {
.setSmallIcon(R.drawable.ic_mountains) .setSmallIcon(R.drawable.ic_mountains)
.setOngoing(true) .setOngoing(true)
.setAutoCancel(false) .setAutoCancel(false)
.setPriority(NotificationCompat.PRIORITY_LOW) .setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setCategory(NotificationCompat.CATEGORY_SERVICE) .setCategory(NotificationCompat.CATEGORY_SERVICE)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC) .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setContentIntent(createOpenAppIntent()) .setContentIntent(createOpenAppIntent())
@@ -221,24 +233,13 @@ class SessionTrackingService : Service() {
) )
.build() .build()
// Always start foreground to ensure service stays alive
startForeground(NOTIFICATION_ID, notification) startForeground(NOTIFICATION_ID, notification)
// Also notify separately to ensure it's visible
notificationManager.notify(NOTIFICATION_ID, notification) notificationManager.notify(NOTIFICATION_ID, notification)
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
// Don't stop the service on notification errors, just log them throw e
// Try to restart the notification after a delay
try {
delay(5000L)
updateNotification(sessionId)
} catch (retryException: Exception) {
retryException.printStackTrace()
// If retry fails, stop the service to prevent infinite loops
stopSessionTracking()
}
} }
} }
@@ -269,50 +270,23 @@ class SessionTrackingService : Service() {
val channel = NotificationChannel( val channel = NotificationChannel(
CHANNEL_ID, CHANNEL_ID,
"Session Tracking", "Session Tracking",
NotificationManager.IMPORTANCE_LOW NotificationManager.IMPORTANCE_DEFAULT
).apply { ).apply {
description = "Shows active climbing session information" description = "Shows active climbing session information"
setShowBadge(false) setShowBadge(false)
lockscreenVisibility = NotificationCompat.VISIBILITY_PUBLIC lockscreenVisibility = NotificationCompat.VISIBILITY_PUBLIC
enableLights(false) enableLights(false)
enableVibration(false) enableVibration(false)
setSound(null, null)
} }
notificationManager.createNotificationChannel(channel) notificationManager.createNotificationChannel(channel)
} }
private fun acquireWakeLock() {
try {
val powerManager = getSystemService(Context.POWER_SERVICE) as PowerManager
wakeLock = powerManager.newWakeLock(
PowerManager.PARTIAL_WAKE_LOCK,
"OpenClimb:SessionTrackingWakeLock"
).apply {
acquire(10*60*1000L) // 10 minutes timeout
}
} catch (e: Exception) {
e.printStackTrace()
}
}
private fun releaseWakeLock() {
try {
wakeLock?.let {
if (it.isHeld) {
it.release()
}
}
wakeLock = null
} catch (e: Exception) {
e.printStackTrace()
}
}
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
notificationJob?.cancel() notificationJob?.cancel()
monitoringJob?.cancel() monitoringJob?.cancel()
releaseWakeLock()
serviceScope.cancel() serviceScope.cancel()
} }
} }

View File

@@ -1,3 +1,4 @@
<resources> <resources>
<string name="app_name">OpenClimb</string> <string name="app_name">OpenClimb</string>
<string name="session_tracking_service_description">Tracks active climbing sessions and displays session information in the notification area</string>
</resources> </resources>