feat: enhance SyncService and MainActivity for better battery optimization and service reliability
All checks were successful
Build / check-version (push) Successful in 6s
Build / build-linux (push) Successful in 5m32s
Build / build-windows (push) Successful in 8m23s
Build / build-android (push) Successful in 11m26s
Build / create-release (push) Has been skipped

This commit is contained in:
2026-02-05 07:48:09 +11:00
parent 89b0da0854
commit ece77ccba6
3 changed files with 200 additions and 33 deletions

View File

@@ -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() {
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")
}
}
}
}

View File

@@ -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)
} else {
startService(restartServiceIntent)
try {
startForegroundService(restartIntent)
} catch (e: Exception) {
Log.e(TAG, "Failed to restart service directly", e)
// Fall back to alarm
scheduleAlarmRestart()
}
} else {
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 scheduleKeepAlive() {
// Use handler for periodic wake lock refresh
handler.postDelayed(keepAliveRunnable, KEEP_ALIVE_INTERVAL_MS)
private fun refreshNotification() {
val notificationManager = getSystemService(NotificationManager::class.java)
notificationManager.notify(NOTIFICATION_ID, createNotification())
}
// Also set up alarm as backup
private fun scheduleKeepAlive() {
handler.postDelayed(keepAliveRunnable, KEEP_ALIVE_INTERVAL_MS)
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(
// 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,
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

View File

@@ -213,9 +213,27 @@ pub fn run() {
.plugin(tauri_plugin_notification::init())
.plugin(tauri_plugin_deep_link::init())
.setup(|app| {
// Create the main window for mobile
// Mobile uses the bundled assets directly, not localhost
// Create the main window for mobile with navigation handler for external links
WebviewWindowBuilder::new(app, "main", WebviewUrl::default())
.on_navigation(|url| {
let url_str = url.as_str();
// Allow navigation to app resources and special protocols
if url_str.starts_with("tauri://")
|| url_str.starts_with("http://tauri.localhost")
|| url_str.starts_with("https://tauri.localhost")
|| url_str.starts_with("blob:")
|| url_str.starts_with("data:")
|| url_str.starts_with("about:")
{
return true;
}
// External URLs - open in default browser
if url_str.starts_with("http://") || url_str.starts_with("https://") {
let _ = tauri_plugin_opener::open_url(url_str, None::<&str>);
return false;
}
true
})
.build()?;
Ok(())
})