From ece77ccba6bb5913103bcf474fbfcb99bda04d41 Mon Sep 17 00:00:00 2001 From: Max Litruv Boonzaayer Date: Thu, 5 Feb 2026 07:48:09 +1100 Subject: [PATCH] feat: enhance SyncService and MainActivity for better battery optimization and service reliability --- .../main/java/wtf/ruv/paarrot/MainActivity.kt | 58 ++++++- .../main/java/wtf/ruv/paarrot/SyncService.kt | 153 +++++++++++++++--- src-tauri/src/lib.rs | 22 ++- 3 files changed, 200 insertions(+), 33 deletions(-) diff --git a/src-tauri/gen/android/app/src/main/java/wtf/ruv/paarrot/MainActivity.kt b/src-tauri/gen/android/app/src/main/java/wtf/ruv/paarrot/MainActivity.kt index a45d8fb..9ee7c6d 100644 --- a/src-tauri/gen/android/app/src/main/java/wtf/ruv/paarrot/MainActivity.kt +++ b/src-tauri/gen/android/app/src/main/java/wtf/ruv/paarrot/MainActivity.kt @@ -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") + } } } } diff --git a/src-tauri/gen/android/app/src/main/java/wtf/ruv/paarrot/SyncService.kt b/src-tauri/gen/android/app/src/main/java/wtf/ruv/paarrot/SyncService.kt index 25eb4db..e9a550b 100644 --- a/src-tauri/gen/android/app/src/main/java/wtf/ruv/paarrot/SyncService.kt +++ b/src-tauri/gen/android/app/src/main/java/wtf/ruv/paarrot/SyncService.kt @@ -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 diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 377d19c..8fbab0d 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -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(()) })