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
|
package wtf.ruv.paarrot
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.os.PowerManager
|
||||||
|
import android.provider.Settings
|
||||||
|
import android.util.Log
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
|
|
||||||
class MainActivity : TauriActivity() {
|
class MainActivity : TauriActivity() {
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "PaarrotMain"
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
// Start the foreground sync service to keep the app alive for notifications
|
// Start the foreground sync service to keep the app alive for notifications
|
||||||
startSyncService()
|
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() {
|
private fun startSyncService() {
|
||||||
|
try {
|
||||||
val serviceIntent = Intent(this, SyncService::class.java)
|
val serviceIntent = Intent(this, SyncService::class.java)
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
startForegroundService(serviceIntent)
|
startForegroundService(serviceIntent)
|
||||||
} else {
|
} else {
|
||||||
startService(serviceIntent)
|
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.app.Service
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.os.PowerManager
|
import android.os.PowerManager
|
||||||
import android.os.SystemClock
|
import android.os.SystemClock
|
||||||
|
import android.provider.Settings
|
||||||
|
import android.util.Log
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -21,20 +24,26 @@ import androidx.core.app.NotificationCompat
|
|||||||
*/
|
*/
|
||||||
class SyncService : Service() {
|
class SyncService : Service() {
|
||||||
companion object {
|
companion object {
|
||||||
|
private const val TAG = "PaarrotSync"
|
||||||
private const val NOTIFICATION_ID = 1
|
private const val NOTIFICATION_ID = 1
|
||||||
private const val CHANNEL_ID = "paarrot_sync_channel"
|
private const val CHANNEL_ID = "paarrot_sync_channel"
|
||||||
private const val CHANNEL_NAME = "Background Sync"
|
private const val CHANNEL_NAME = "Background Sync"
|
||||||
private const val WAKE_LOCK_TAG = "Paarrot:SyncWakeLock"
|
private const val WAKE_LOCK_TAG = "Paarrot:SyncWakeLock"
|
||||||
private const val ALARM_REQUEST_CODE = 1001
|
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 var wakeLock: PowerManager.WakeLock? = null
|
||||||
private val handler = Handler(Looper.getMainLooper())
|
private val handler = Handler(Looper.getMainLooper())
|
||||||
|
private var isRunning = false
|
||||||
|
|
||||||
private val keepAliveRunnable = object : Runnable {
|
private val keepAliveRunnable = object : Runnable {
|
||||||
override fun run() {
|
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()
|
ensureWakeLock()
|
||||||
|
refreshNotification()
|
||||||
handler.postDelayed(this, KEEP_ALIVE_INTERVAL_MS)
|
handler.postDelayed(this, KEEP_ALIVE_INTERVAL_MS)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -43,35 +52,103 @@ class SyncService : Service() {
|
|||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
Log.d(TAG, "SyncService created")
|
||||||
|
isRunning = true
|
||||||
createNotificationChannel()
|
createNotificationChannel()
|
||||||
acquireWakeLock()
|
acquireWakeLock()
|
||||||
scheduleKeepAlive()
|
scheduleKeepAlive()
|
||||||
|
requestBatteryOptimizationExemption()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
|
Log.d(TAG, "SyncService onStartCommand")
|
||||||
startForeground(NOTIFICATION_ID, createNotification())
|
startForeground(NOTIFICATION_ID, createNotification())
|
||||||
ensureWakeLock()
|
ensureWakeLock()
|
||||||
return START_STICKY
|
// Return REDELIVER_INTENT so the system restarts us with the last intent
|
||||||
|
return START_REDELIVER_INTENT
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
|
Log.d(TAG, "SyncService being destroyed - scheduling restart")
|
||||||
|
isRunning = false
|
||||||
handler.removeCallbacks(keepAliveRunnable)
|
handler.removeCallbacks(keepAliveRunnable)
|
||||||
cancelAlarm()
|
|
||||||
|
// Schedule immediate restart
|
||||||
|
scheduleRestart()
|
||||||
|
|
||||||
releaseWakeLock()
|
releaseWakeLock()
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onTaskRemoved(rootIntent: Intent?) {
|
override fun onTaskRemoved(rootIntent: Intent?) {
|
||||||
// Restart service if app is swiped away
|
Log.d(TAG, "Task removed - scheduling restart")
|
||||||
val restartServiceIntent = Intent(applicationContext, SyncService::class.java)
|
scheduleRestart()
|
||||||
restartServiceIntent.setPackage(packageName)
|
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) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
startForegroundService(restartServiceIntent)
|
try {
|
||||||
} else {
|
startForegroundService(restartIntent)
|
||||||
startService(restartServiceIntent)
|
} 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() {
|
private fun createNotificationChannel() {
|
||||||
@@ -83,6 +160,8 @@ class SyncService : Service() {
|
|||||||
).apply {
|
).apply {
|
||||||
description = "Keeps Paarrot connected for message notifications"
|
description = "Keeps Paarrot connected for message notifications"
|
||||||
setShowBadge(false)
|
setShowBadge(false)
|
||||||
|
enableLights(false)
|
||||||
|
enableVibration(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
val notificationManager = getSystemService(NotificationManager::class.java)
|
val notificationManager = getSystemService(NotificationManager::class.java)
|
||||||
@@ -94,8 +173,10 @@ class SyncService : Service() {
|
|||||||
val pendingIntent = PendingIntent.getActivity(
|
val pendingIntent = PendingIntent.getActivity(
|
||||||
this,
|
this,
|
||||||
0,
|
0,
|
||||||
Intent(this, MainActivity::class.java),
|
Intent(this, MainActivity::class.java).apply {
|
||||||
PendingIntent.FLAG_IMMUTABLE
|
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)
|
return NotificationCompat.Builder(this, CHANNEL_ID)
|
||||||
@@ -105,14 +186,18 @@ class SyncService : Service() {
|
|||||||
.setContentIntent(pendingIntent)
|
.setContentIntent(pendingIntent)
|
||||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||||
.setOngoing(true)
|
.setOngoing(true)
|
||||||
|
.setShowWhen(false)
|
||||||
|
.setCategory(NotificationCompat.CATEGORY_SERVICE)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun scheduleKeepAlive() {
|
private fun refreshNotification() {
|
||||||
// Use handler for periodic wake lock refresh
|
val notificationManager = getSystemService(NotificationManager::class.java)
|
||||||
handler.postDelayed(keepAliveRunnable, KEEP_ALIVE_INTERVAL_MS)
|
notificationManager.notify(NOTIFICATION_ID, createNotification())
|
||||||
|
}
|
||||||
|
|
||||||
// Also set up alarm as backup
|
private fun scheduleKeepAlive() {
|
||||||
|
handler.postDelayed(keepAliveRunnable, KEEP_ALIVE_INTERVAL_MS)
|
||||||
scheduleAlarm()
|
scheduleAlarm()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,13 +211,20 @@ class SyncService : Service() {
|
|||||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||||
)
|
)
|
||||||
|
|
||||||
// Schedule inexact repeating alarm to restart service periodically
|
// Use setExactAndAllowWhileIdle for better Doze mode support
|
||||||
alarmManager.setInexactRepeating(
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
alarmManager.setExactAndAllowWhileIdle(
|
||||||
AlarmManager.ELAPSED_REALTIME_WAKEUP,
|
AlarmManager.ELAPSED_REALTIME_WAKEUP,
|
||||||
SystemClock.elapsedRealtime() + KEEP_ALIVE_INTERVAL_MS,
|
SystemClock.elapsedRealtime() + KEEP_ALIVE_INTERVAL_MS,
|
||||||
KEEP_ALIVE_INTERVAL_MS,
|
|
||||||
pendingIntent
|
pendingIntent
|
||||||
)
|
)
|
||||||
|
} else {
|
||||||
|
alarmManager.setExact(
|
||||||
|
AlarmManager.ELAPSED_REALTIME_WAKEUP,
|
||||||
|
SystemClock.elapsedRealtime() + KEEP_ALIVE_INTERVAL_MS,
|
||||||
|
pendingIntent
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun cancelAlarm() {
|
private fun cancelAlarm() {
|
||||||
@@ -153,23 +245,32 @@ class SyncService : Service() {
|
|||||||
PowerManager.PARTIAL_WAKE_LOCK,
|
PowerManager.PARTIAL_WAKE_LOCK,
|
||||||
WAKE_LOCK_TAG
|
WAKE_LOCK_TAG
|
||||||
).apply {
|
).apply {
|
||||||
// Acquire indefinitely - the service manages its own lifecycle
|
// Acquire with timeout to prevent battery drain if something goes wrong
|
||||||
acquire()
|
acquire(WAKE_LOCK_TIMEOUT_MS)
|
||||||
}
|
}
|
||||||
|
Log.d(TAG, "Wake lock acquired")
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun ensureWakeLock() {
|
private fun ensureWakeLock() {
|
||||||
wakeLock?.let {
|
wakeLock?.let {
|
||||||
if (!it.isHeld) {
|
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() {
|
private fun releaseWakeLock() {
|
||||||
wakeLock?.let {
|
wakeLock?.let {
|
||||||
if (it.isHeld) {
|
if (it.isHeld) {
|
||||||
it.release()
|
it.release()
|
||||||
|
Log.d(TAG, "Wake lock released")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
wakeLock = null
|
wakeLock = null
|
||||||
|
|||||||
@@ -213,9 +213,27 @@ pub fn run() {
|
|||||||
.plugin(tauri_plugin_notification::init())
|
.plugin(tauri_plugin_notification::init())
|
||||||
.plugin(tauri_plugin_deep_link::init())
|
.plugin(tauri_plugin_deep_link::init())
|
||||||
.setup(|app| {
|
.setup(|app| {
|
||||||
// Create the main window for mobile
|
// Create the main window for mobile with navigation handler for external links
|
||||||
// Mobile uses the bundled assets directly, not localhost
|
|
||||||
WebviewWindowBuilder::new(app, "main", WebviewUrl::default())
|
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()?;
|
.build()?;
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user