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:
@@ -7,8 +7,10 @@
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||
|
||||
<application
|
||||
android:name=".SimpleNotesApplication"
|
||||
android:allowBackup="true"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
@@ -39,16 +41,6 @@
|
||||
android:name=".SettingsActivity"
|
||||
android:parentActivityName=".MainActivity" />
|
||||
|
||||
<!-- WiFi Sync Receiver -->
|
||||
<receiver
|
||||
android:name=".sync.WifiSyncReceiver"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.net.wifi.STATE_CHANGE" />
|
||||
<action android:name="android.net.conn.CONNECTIVITY_CHANGE" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -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)
|
||||
|
||||
@@ -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 -> {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
if (result.isSuccess) {
|
||||
Log.d(TAG, "✅ Sync successful: ${result.syncedCount} notes")
|
||||
NotificationHelper.showSyncSuccess(
|
||||
applicationContext,
|
||||
syncResult.conflictCount
|
||||
result.syncedCount
|
||||
)
|
||||
Result.success()
|
||||
}
|
||||
syncResult.isSuccess -> {
|
||||
// Erfolgs-Notification
|
||||
NotificationHelper.showSyncSuccessNotification(
|
||||
} else {
|
||||
Log.e(TAG, "❌ Sync failed: ${result.errorMessage}")
|
||||
NotificationHelper.showSyncError(
|
||||
applicationContext,
|
||||
syncResult.syncedCount
|
||||
result.errorMessage ?: "Unbekannter Fehler"
|
||||
)
|
||||
Result.success()
|
||||
Result.failure()
|
||||
}
|
||||
else -> {
|
||||
// Fehler-Notification
|
||||
NotificationHelper.showSyncFailureNotification(
|
||||
applicationContext,
|
||||
syncResult.errorMessage ?: "Unbekannter Fehler"
|
||||
)
|
||||
Result.retry()
|
||||
}
|
||||
}
|
||||
|
||||
} 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
0
android/gradlew
vendored
Normal file → Executable file
0
android/gradlew
vendored
Normal file → Executable file
Reference in New Issue
Block a user