feat: enhance SyncService and MainActivity for better battery optimization and service reliability
All checks were successful
All checks were successful
This commit is contained in:
@@ -1,25 +1,73 @@
|
||||
package wtf.ruv.paarrot
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.PowerManager
|
||||
import android.provider.Settings
|
||||
import android.util.Log
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
|
||||
class MainActivity : TauriActivity() {
|
||||
companion object {
|
||||
private const val TAG = "PaarrotMain"
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
enableEdgeToEdge()
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
// Start the foreground sync service to keep the app alive for notifications
|
||||
startSyncService()
|
||||
|
||||
// Request battery optimization exemption for reliable background operation
|
||||
requestBatteryOptimizationExemption()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
// Ensure service is running when app comes to foreground
|
||||
startSyncService()
|
||||
}
|
||||
|
||||
private fun startSyncService() {
|
||||
val serviceIntent = Intent(this, SyncService::class.java)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
startForegroundService(serviceIntent)
|
||||
} else {
|
||||
startService(serviceIntent)
|
||||
try {
|
||||
val serviceIntent = Intent(this, SyncService::class.java)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
startForegroundService(serviceIntent)
|
||||
} else {
|
||||
startService(serviceIntent)
|
||||
}
|
||||
Log.d(TAG, "SyncService started")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to start SyncService", e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun requestBatteryOptimizationExemption() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
val powerManager = getSystemService(POWER_SERVICE) as PowerManager
|
||||
if (!powerManager.isIgnoringBatteryOptimizations(packageName)) {
|
||||
Log.d(TAG, "Requesting battery optimization exemption")
|
||||
try {
|
||||
val intent = Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply {
|
||||
data = Uri.parse("package:$packageName")
|
||||
}
|
||||
startActivity(intent)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to request battery optimization exemption", e)
|
||||
// Try opening battery settings instead
|
||||
try {
|
||||
val settingsIntent = Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS)
|
||||
startActivity(settingsIntent)
|
||||
} catch (e2: Exception) {
|
||||
Log.e(TAG, "Failed to open battery settings", e2)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Log.d(TAG, "Already exempt from battery optimization")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,12 +7,15 @@ import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.IBinder
|
||||
import android.os.Looper
|
||||
import android.os.PowerManager
|
||||
import android.os.SystemClock
|
||||
import android.provider.Settings
|
||||
import android.util.Log
|
||||
import androidx.core.app.NotificationCompat
|
||||
|
||||
/**
|
||||
@@ -21,20 +24,26 @@ import androidx.core.app.NotificationCompat
|
||||
*/
|
||||
class SyncService : Service() {
|
||||
companion object {
|
||||
private const val TAG = "PaarrotSync"
|
||||
private const val NOTIFICATION_ID = 1
|
||||
private const val CHANNEL_ID = "paarrot_sync_channel"
|
||||
private const val CHANNEL_NAME = "Background Sync"
|
||||
private const val WAKE_LOCK_TAG = "Paarrot:SyncWakeLock"
|
||||
private const val ALARM_REQUEST_CODE = 1001
|
||||
private const val KEEP_ALIVE_INTERVAL_MS = 5 * 60 * 1000L // 5 minutes
|
||||
private const val KEEP_ALIVE_INTERVAL_MS = 4 * 60 * 1000L // 4 minutes (before 5 min Doze threshold)
|
||||
private const val WAKE_LOCK_TIMEOUT_MS = 10 * 60 * 1000L // 10 minutes max wake lock
|
||||
}
|
||||
|
||||
private var wakeLock: PowerManager.WakeLock? = null
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
private var isRunning = false
|
||||
|
||||
private val keepAliveRunnable = object : Runnable {
|
||||
override fun run() {
|
||||
// Re-acquire wake lock periodically to prevent it from being released
|
||||
if (!isRunning) return
|
||||
Log.d(TAG, "Keep-alive tick - refreshing wake lock")
|
||||
ensureWakeLock()
|
||||
refreshNotification()
|
||||
handler.postDelayed(this, KEEP_ALIVE_INTERVAL_MS)
|
||||
}
|
||||
}
|
||||
@@ -43,35 +52,103 @@ class SyncService : Service() {
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
Log.d(TAG, "SyncService created")
|
||||
isRunning = true
|
||||
createNotificationChannel()
|
||||
acquireWakeLock()
|
||||
scheduleKeepAlive()
|
||||
requestBatteryOptimizationExemption()
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
Log.d(TAG, "SyncService onStartCommand")
|
||||
startForeground(NOTIFICATION_ID, createNotification())
|
||||
ensureWakeLock()
|
||||
return START_STICKY
|
||||
// Return REDELIVER_INTENT so the system restarts us with the last intent
|
||||
return START_REDELIVER_INTENT
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
Log.d(TAG, "SyncService being destroyed - scheduling restart")
|
||||
isRunning = false
|
||||
handler.removeCallbacks(keepAliveRunnable)
|
||||
cancelAlarm()
|
||||
|
||||
// Schedule immediate restart
|
||||
scheduleRestart()
|
||||
|
||||
releaseWakeLock()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onTaskRemoved(rootIntent: Intent?) {
|
||||
// Restart service if app is swiped away
|
||||
val restartServiceIntent = Intent(applicationContext, SyncService::class.java)
|
||||
restartServiceIntent.setPackage(packageName)
|
||||
Log.d(TAG, "Task removed - scheduling restart")
|
||||
scheduleRestart()
|
||||
super.onTaskRemoved(rootIntent)
|
||||
}
|
||||
|
||||
override fun onLowMemory() {
|
||||
Log.w(TAG, "Low memory warning")
|
||||
super.onLowMemory()
|
||||
}
|
||||
|
||||
override fun onTrimMemory(level: Int) {
|
||||
Log.d(TAG, "Trim memory level: $level")
|
||||
super.onTrimMemory(level)
|
||||
}
|
||||
|
||||
private fun scheduleRestart() {
|
||||
// Try to restart the service immediately
|
||||
val restartIntent = Intent(applicationContext, SyncService::class.java)
|
||||
restartIntent.setPackage(packageName)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
startForegroundService(restartServiceIntent)
|
||||
try {
|
||||
startForegroundService(restartIntent)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to restart service directly", e)
|
||||
// Fall back to alarm
|
||||
scheduleAlarmRestart()
|
||||
}
|
||||
} else {
|
||||
startService(restartServiceIntent)
|
||||
startService(restartIntent)
|
||||
}
|
||||
}
|
||||
|
||||
private fun scheduleAlarmRestart() {
|
||||
val alarmManager = getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
||||
val intent = Intent(this, SyncService::class.java)
|
||||
val pendingIntent = PendingIntent.getService(
|
||||
this,
|
||||
ALARM_REQUEST_CODE + 1,
|
||||
intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
|
||||
// Schedule restart in 1 second
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
alarmManager.setExactAndAllowWhileIdle(
|
||||
AlarmManager.ELAPSED_REALTIME_WAKEUP,
|
||||
SystemClock.elapsedRealtime() + 1000,
|
||||
pendingIntent
|
||||
)
|
||||
} else {
|
||||
alarmManager.setExact(
|
||||
AlarmManager.ELAPSED_REALTIME_WAKEUP,
|
||||
SystemClock.elapsedRealtime() + 1000,
|
||||
pendingIntent
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun requestBatteryOptimizationExemption() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
val powerManager = getSystemService(Context.POWER_SERVICE) as PowerManager
|
||||
if (!powerManager.isIgnoringBatteryOptimizations(packageName)) {
|
||||
Log.d(TAG, "Requesting battery optimization exemption")
|
||||
// We can't directly request, but the app should prompt the user
|
||||
// This is just a note that we need the exemption
|
||||
}
|
||||
}
|
||||
super.onTaskRemoved(rootIntent)
|
||||
}
|
||||
|
||||
private fun createNotificationChannel() {
|
||||
@@ -83,6 +160,8 @@ class SyncService : Service() {
|
||||
).apply {
|
||||
description = "Keeps Paarrot connected for message notifications"
|
||||
setShowBadge(false)
|
||||
enableLights(false)
|
||||
enableVibration(false)
|
||||
}
|
||||
|
||||
val notificationManager = getSystemService(NotificationManager::class.java)
|
||||
@@ -94,8 +173,10 @@ class SyncService : Service() {
|
||||
val pendingIntent = PendingIntent.getActivity(
|
||||
this,
|
||||
0,
|
||||
Intent(this, MainActivity::class.java),
|
||||
PendingIntent.FLAG_IMMUTABLE
|
||||
Intent(this, MainActivity::class.java).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
},
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
|
||||
return NotificationCompat.Builder(this, CHANNEL_ID)
|
||||
@@ -105,14 +186,18 @@ class SyncService : Service() {
|
||||
.setContentIntent(pendingIntent)
|
||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||
.setOngoing(true)
|
||||
.setShowWhen(false)
|
||||
.setCategory(NotificationCompat.CATEGORY_SERVICE)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun refreshNotification() {
|
||||
val notificationManager = getSystemService(NotificationManager::class.java)
|
||||
notificationManager.notify(NOTIFICATION_ID, createNotification())
|
||||
}
|
||||
|
||||
private fun scheduleKeepAlive() {
|
||||
// Use handler for periodic wake lock refresh
|
||||
handler.postDelayed(keepAliveRunnable, KEEP_ALIVE_INTERVAL_MS)
|
||||
|
||||
// Also set up alarm as backup
|
||||
scheduleAlarm()
|
||||
}
|
||||
|
||||
@@ -126,13 +211,20 @@ class SyncService : Service() {
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
|
||||
// Schedule inexact repeating alarm to restart service periodically
|
||||
alarmManager.setInexactRepeating(
|
||||
AlarmManager.ELAPSED_REALTIME_WAKEUP,
|
||||
SystemClock.elapsedRealtime() + KEEP_ALIVE_INTERVAL_MS,
|
||||
KEEP_ALIVE_INTERVAL_MS,
|
||||
pendingIntent
|
||||
)
|
||||
// Use setExactAndAllowWhileIdle for better Doze mode support
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
alarmManager.setExactAndAllowWhileIdle(
|
||||
AlarmManager.ELAPSED_REALTIME_WAKEUP,
|
||||
SystemClock.elapsedRealtime() + KEEP_ALIVE_INTERVAL_MS,
|
||||
pendingIntent
|
||||
)
|
||||
} else {
|
||||
alarmManager.setExact(
|
||||
AlarmManager.ELAPSED_REALTIME_WAKEUP,
|
||||
SystemClock.elapsedRealtime() + KEEP_ALIVE_INTERVAL_MS,
|
||||
pendingIntent
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun cancelAlarm() {
|
||||
@@ -153,23 +245,32 @@ class SyncService : Service() {
|
||||
PowerManager.PARTIAL_WAKE_LOCK,
|
||||
WAKE_LOCK_TAG
|
||||
).apply {
|
||||
// Acquire indefinitely - the service manages its own lifecycle
|
||||
acquire()
|
||||
// Acquire with timeout to prevent battery drain if something goes wrong
|
||||
acquire(WAKE_LOCK_TIMEOUT_MS)
|
||||
}
|
||||
Log.d(TAG, "Wake lock acquired")
|
||||
}
|
||||
|
||||
private fun ensureWakeLock() {
|
||||
wakeLock?.let {
|
||||
if (!it.isHeld) {
|
||||
it.acquire()
|
||||
Log.d(TAG, "Wake lock was released, re-acquiring")
|
||||
it.acquire(WAKE_LOCK_TIMEOUT_MS)
|
||||
}
|
||||
} ?: acquireWakeLock()
|
||||
} ?: run {
|
||||
Log.d(TAG, "Wake lock was null, creating new one")
|
||||
acquireWakeLock()
|
||||
}
|
||||
|
||||
// Re-schedule alarm each time we refresh
|
||||
scheduleAlarm()
|
||||
}
|
||||
|
||||
private fun releaseWakeLock() {
|
||||
wakeLock?.let {
|
||||
if (it.isHeld) {
|
||||
it.release()
|
||||
Log.d(TAG, "Wake lock released")
|
||||
}
|
||||
}
|
||||
wakeLock = null
|
||||
|
||||
Reference in New Issue
Block a user