Add Application-level NetworkMonitor with extensive logging

- Create SimpleNotesApplication class for app-level lifecycle
- Move NetworkMonitor from Activity to Application context
- Add comprehensive logging to NetworkMonitor (all callbacks)
- Add logging to SyncWorker for debugging
- Remove NetworkMonitor from MainActivity (now in Application)
- Add Battery Optimization dialog in SettingsActivity
- Improve Notifications (showSyncInProgress, showSyncSuccess, showSyncError)

This should fix background sync issues - NetworkMonitor now runs
with Application context instead of Activity context, which should
survive when app is in background.

Debug with: adb logcat | grep -E 'NetworkMonitor|SyncWorker|SimpleNotesApp'
This commit is contained in:
inventory69
2025-12-20 17:19:45 +01:00
parent 4eb8a006dd
commit 980343866f
8 changed files with 377 additions and 53 deletions

View File

@@ -17,6 +17,11 @@ import dev.dettmer.simplenotes.storage.NotesStorage
import dev.dettmer.simplenotes.utils.NotificationHelper
import dev.dettmer.simplenotes.utils.showToast
import android.widget.TextView
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import dev.dettmer.simplenotes.sync.WebDavSyncService
class MainActivity : AppCompatActivity() {
@@ -36,9 +41,6 @@ class MainActivity : AppCompatActivity() {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// Notification Channel erstellen
NotificationHelper.createNotificationChannel(this)
// Permission für Notifications (Android 13+)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
requestNotificationPermission()
@@ -106,6 +108,31 @@ class MainActivity : AppCompatActivity() {
startActivity(Intent(this, SettingsActivity::class.java))
}
private fun triggerManualSync() {
lifecycleScope.launch {
try {
showToast("Starte Synchronisation...")
// Start sync
val syncService = WebDavSyncService(this@MainActivity)
val result = withContext(Dispatchers.IO) {
syncService.syncNotes()
}
// Show result
if (result.isSuccess) {
showToast("Sync erfolgreich: ${result.syncedCount} Notizen")
loadNotes() // Reload notes
} else {
showToast("Sync Fehler: ${result.errorMessage}")
}
} catch (e: Exception) {
showToast("Sync Fehler: ${e.message}")
}
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu_main, menu)
return true
@@ -118,8 +145,7 @@ class MainActivity : AppCompatActivity() {
true
}
R.id.action_sync -> {
// Manual sync trigger could be added here
showToast("Sync wird in den Einstellungen gestartet")
triggerManualSync()
true
}
else -> super.onOptionsItemSelected(item)

View File

@@ -1,13 +1,19 @@
package dev.dettmer.simplenotes
import android.Manifest
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.net.wifi.WifiManager
import android.os.Build
import android.os.Bundle
import android.os.PowerManager
import android.provider.Settings
import android.view.MenuItem
import android.widget.Button
import android.widget.EditText
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.SwitchCompat
import androidx.core.app.ActivityCompat
@@ -88,6 +94,10 @@ class SettingsActivity : AppCompatActivity() {
buttonDetectSSID.setOnClickListener {
detectCurrentSSID()
}
switchAutoSync.setOnCheckedChangeListener { _, isChecked ->
onAutoSyncToggled(isChecked)
}
}
private fun saveSettings() {
@@ -192,6 +202,61 @@ class SettingsActivity : AppCompatActivity() {
}
}
private fun onAutoSyncToggled(enabled: Boolean) {
prefs.edit().putBoolean(Constants.KEY_AUTO_SYNC, enabled).apply()
if (enabled) {
showToast("Auto-Sync aktiviert")
// Check battery optimization when enabling
checkBatteryOptimization()
} else {
showToast("Auto-Sync deaktiviert")
}
}
private fun checkBatteryOptimization() {
val powerManager = getSystemService(Context.POWER_SERVICE) as PowerManager
val packageName = packageName
if (!powerManager.isIgnoringBatteryOptimizations(packageName)) {
showBatteryOptimizationDialog()
}
}
private fun showBatteryOptimizationDialog() {
AlertDialog.Builder(this)
.setTitle("Hintergrund-Synchronisation")
.setMessage(
"Damit die App im Hintergrund synchronisieren kann, " +
"muss die Akku-Optimierung deaktiviert werden.\n\n" +
"Bitte wähle 'Nicht optimieren' für Simple Notes."
)
.setPositiveButton("Einstellungen öffnen") { _, _ ->
openBatteryOptimizationSettings()
}
.setNegativeButton("Später") { dialog, _ ->
dialog.dismiss()
}
.setCancelable(false)
.show()
}
private fun openBatteryOptimizationSettings() {
try {
val intent = Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS)
intent.data = Uri.parse("package:$packageName")
startActivity(intent)
} catch (e: Exception) {
// Fallback: Open general battery settings
try {
val intent = Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS)
startActivity(intent)
} catch (e2: Exception) {
showToast("Bitte Akku-Optimierung manuell deaktivieren")
}
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
android.R.id.home -> {

View File

@@ -0,0 +1,41 @@
package dev.dettmer.simplenotes
import android.app.Application
import android.util.Log
import dev.dettmer.simplenotes.sync.NetworkMonitor
import dev.dettmer.simplenotes.utils.NotificationHelper
class SimpleNotesApplication : Application() {
companion object {
private const val TAG = "SimpleNotesApp"
}
private lateinit var networkMonitor: NetworkMonitor
override fun onCreate() {
super.onCreate()
Log.d(TAG, "🚀 Application onCreate()")
// Initialize notification channel
NotificationHelper.createNotificationChannel(this)
Log.d(TAG, "✅ Notification channel created")
// Initialize and start NetworkMonitor at application level
// CRITICAL: Use applicationContext, not 'this'!
networkMonitor = NetworkMonitor(applicationContext)
networkMonitor.startMonitoring()
Log.d(TAG, "✅ NetworkMonitor initialized and started")
}
override fun onTerminate() {
super.onTerminate()
Log.d(TAG, "🛑 Application onTerminate()")
// Clean up NetworkMonitor when app is terminated
networkMonitor.stopMonitoring()
}
}

View File

@@ -0,0 +1,155 @@
package dev.dettmer.simplenotes.sync
import android.content.Context
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import android.net.wifi.WifiInfo
import android.net.wifi.WifiManager
import android.os.Build
import android.util.Log
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.OutOfQuotaPolicy
import androidx.work.WorkManager
import dev.dettmer.simplenotes.utils.Constants
import java.util.concurrent.TimeUnit
class NetworkMonitor(private val context: Context) {
companion object {
private const val TAG = "NetworkMonitor"
}
private val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE)
as ConnectivityManager
private val networkCallback = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
super.onAvailable(network)
Log.d(TAG, "📶 Network available: $network")
val capabilities = connectivityManager.getNetworkCapabilities(network)
if (capabilities?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) == true) {
Log.d(TAG, "✅ WiFi detected")
checkAndTriggerSync()
} else {
Log.d(TAG, "❌ Not WiFi: ${capabilities?.toString()}")
}
}
override fun onCapabilitiesChanged(
network: Network,
capabilities: NetworkCapabilities
) {
super.onCapabilitiesChanged(network, capabilities)
Log.d(TAG, "🔄 Capabilities changed: $network")
if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) {
Log.d(TAG, "✅ WiFi capabilities")
checkAndTriggerSync()
}
}
override fun onLost(network: Network) {
super.onLost(network)
Log.d(TAG, "❌ Network lost: $network")
}
}
fun startMonitoring() {
Log.d(TAG, "🚀 Starting NetworkMonitor")
Log.d(TAG, "Context type: ${context.javaClass.simpleName}")
val request = NetworkRequest.Builder()
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
.build()
try {
connectivityManager.registerNetworkCallback(request, networkCallback)
Log.d(TAG, "✅ NetworkCallback registered successfully")
} catch (e: Exception) {
Log.e(TAG, "❌ Failed to register NetworkCallback: ${e.message}", e)
}
}
fun stopMonitoring() {
Log.d(TAG, "🛑 Stopping NetworkMonitor")
try {
connectivityManager.unregisterNetworkCallback(networkCallback)
Log.d(TAG, "✅ NetworkCallback unregistered")
} catch (e: Exception) {
Log.w(TAG, "⚠️ NetworkCallback already unregistered: ${e.message}")
}
}
private fun checkAndTriggerSync() {
Log.d(TAG, "🔍 Checking auto-sync conditions...")
val prefs = context.getSharedPreferences(Constants.PREFS_NAME, Context.MODE_PRIVATE)
val autoSyncEnabled = prefs.getBoolean(Constants.KEY_AUTO_SYNC, false)
Log.d(TAG, "Auto-sync enabled: $autoSyncEnabled")
if (!autoSyncEnabled) {
Log.d(TAG, "❌ Auto-sync disabled, skipping")
return
}
val homeSSID = prefs.getString(Constants.KEY_HOME_SSID, null)
Log.d(TAG, "Home SSID configured: $homeSSID")
if (isConnectedToHomeWifi()) {
Log.d(TAG, "✅ Connected to home WiFi, scheduling sync!")
scheduleSyncWork()
} else {
Log.d(TAG, "❌ Not connected to home WiFi")
}
}
private fun isConnectedToHomeWifi(): Boolean {
val prefs = context.getSharedPreferences(Constants.PREFS_NAME, Context.MODE_PRIVATE)
val homeSSID = prefs.getString(Constants.KEY_HOME_SSID, null) ?: return false
val wifiManager = context.applicationContext.getSystemService(Context.WIFI_SERVICE)
as WifiManager
val currentSSID = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
// Android 12+: Use WifiInfo from NetworkCapabilities
val network = connectivityManager.activeNetwork ?: return false
val capabilities = connectivityManager.getNetworkCapabilities(network) ?: return false
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val wifiInfo = capabilities.transportInfo as? WifiInfo
wifiInfo?.ssid?.replace("\"", "") ?: ""
} else {
wifiManager.connectionInfo.ssid.replace("\"", "")
}
} else {
wifiManager.connectionInfo.ssid.replace("\"", "")
}
Log.d(TAG, "Current SSID: '$currentSSID', Home SSID: '$homeSSID'")
val isHome = currentSSID == homeSSID
Log.d(TAG, "Is home WiFi: $isHome")
return isHome
}
private fun scheduleSyncWork() {
Log.d(TAG, "📅 Scheduling sync work...")
val syncRequest = OneTimeWorkRequestBuilder<SyncWorker>()
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
.addTag(Constants.SYNC_WORK_TAG)
.build()
WorkManager.getInstance(context).enqueue(syncRequest)
Log.d(TAG, "✅ Sync work scheduled with WorkManager")
}
}

View File

@@ -1,6 +1,7 @@
package dev.dettmer.simplenotes.sync
import android.content.Context
import android.util.Log
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import dev.dettmer.simplenotes.utils.NotificationHelper
@@ -12,54 +13,46 @@ class SyncWorker(
params: WorkerParameters
) : CoroutineWorker(context, params) {
companion object {
private const val TAG = "SyncWorker"
}
override suspend fun doWork(): Result = withContext(Dispatchers.IO) {
// Progress Notification zeigen
val notificationId = NotificationHelper.showSyncProgressNotification(applicationContext)
Log.d(TAG, "🔄 SyncWorker started")
return@withContext try {
// Show notification that sync is starting
NotificationHelper.showSyncInProgress(applicationContext)
Log.d(TAG, "📢 Notification shown: Sync in progress")
// Start sync
val syncService = WebDavSyncService(applicationContext)
val syncResult = syncService.syncNotes()
Log.d(TAG, "🚀 Starting sync...")
// Progress Notification entfernen
NotificationHelper.dismissNotification(applicationContext, notificationId)
val result = syncService.syncNotes()
when {
syncResult.hasConflicts -> {
// Konflikt-Notification
NotificationHelper.showConflictNotification(
applicationContext,
syncResult.conflictCount
)
Result.success()
}
syncResult.isSuccess -> {
// Erfolgs-Notification
NotificationHelper.showSyncSuccessNotification(
applicationContext,
syncResult.syncedCount
)
Result.success()
}
else -> {
// Fehler-Notification
NotificationHelper.showSyncFailureNotification(
applicationContext,
syncResult.errorMessage ?: "Unbekannter Fehler"
)
Result.retry()
}
if (result.isSuccess) {
Log.d(TAG, "✅ Sync successful: ${result.syncedCount} notes")
NotificationHelper.showSyncSuccess(
applicationContext,
result.syncedCount
)
Result.success()
} else {
Log.e(TAG, "❌ Sync failed: ${result.errorMessage}")
NotificationHelper.showSyncError(
applicationContext,
result.errorMessage ?: "Unbekannter Fehler"
)
Result.failure()
}
} catch (e: Exception) {
// Fehler-Notification
NotificationHelper.dismissNotification(applicationContext, notificationId)
NotificationHelper.showSyncFailureNotification(
Log.e(TAG, "💥 Sync exception: ${e.message}", e)
NotificationHelper.showSyncError(
applicationContext,
e.message ?: "Sync fehlgeschlagen"
e.message ?: "Unknown error"
)
// Retry mit Backoff
Result.retry()
Result.failure()
}
}
}

View File

@@ -16,6 +16,7 @@ object NotificationHelper {
private const val CHANNEL_NAME = "Notizen Synchronisierung"
private const val CHANNEL_DESCRIPTION = "Benachrichtigungen über Sync-Status"
private const val NOTIFICATION_ID = 1001
private const val SYNC_NOTIFICATION_ID = 2
/**
* Erstellt Notification Channel (Android 8.0+)
@@ -189,4 +190,55 @@ object NotificationHelper {
true
}
}
/**
* Zeigt Notification dass Sync startet
*/
fun showSyncInProgress(context: Context) {
val notification = NotificationCompat.Builder(context, CHANNEL_ID)
.setSmallIcon(android.R.drawable.stat_notify_sync)
.setContentTitle("Synchronisierung läuft")
.setContentText("Notizen werden synchronisiert...")
.setPriority(NotificationCompat.PRIORITY_LOW)
.setOngoing(true)
.build()
val manager = context.getSystemService(Context.NOTIFICATION_SERVICE)
as NotificationManager
manager.notify(SYNC_NOTIFICATION_ID, notification)
}
/**
* Zeigt Erfolgs-Notification
*/
fun showSyncSuccess(context: Context, count: Int) {
val notification = NotificationCompat.Builder(context, CHANNEL_ID)
.setSmallIcon(android.R.drawable.stat_notify_sync)
.setContentTitle("Sync erfolgreich")
.setContentText("$count Notizen synchronisiert")
.setPriority(NotificationCompat.PRIORITY_LOW)
.setAutoCancel(true)
.build()
val manager = context.getSystemService(Context.NOTIFICATION_SERVICE)
as NotificationManager
manager.notify(SYNC_NOTIFICATION_ID, notification)
}
/**
* Zeigt Fehler-Notification
*/
fun showSyncError(context: Context, message: String) {
val notification = NotificationCompat.Builder(context, CHANNEL_ID)
.setSmallIcon(android.R.drawable.stat_notify_error)
.setContentTitle("Sync Fehler")
.setContentText(message)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setAutoCancel(true)
.build()
val manager = context.getSystemService(Context.NOTIFICATION_SERVICE)
as NotificationManager
manager.notify(SYNC_NOTIFICATION_ID, notification)
}
}