diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 9e0389f..cf7a91e 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -7,8 +7,10 @@
+
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/MainActivity.kt b/android/app/src/main/java/dev/dettmer/simplenotes/MainActivity.kt
index a9a7779..41ed7c9 100644
--- a/android/app/src/main/java/dev/dettmer/simplenotes/MainActivity.kt
+++ b/android/app/src/main/java/dev/dettmer/simplenotes/MainActivity.kt
@@ -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)
diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/SettingsActivity.kt b/android/app/src/main/java/dev/dettmer/simplenotes/SettingsActivity.kt
index 040c2eb..d32444a 100644
--- a/android/app/src/main/java/dev/dettmer/simplenotes/SettingsActivity.kt
+++ b/android/app/src/main/java/dev/dettmer/simplenotes/SettingsActivity.kt
@@ -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 -> {
diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/SimpleNotesApplication.kt b/android/app/src/main/java/dev/dettmer/simplenotes/SimpleNotesApplication.kt
new file mode 100644
index 0000000..2b1015e
--- /dev/null
+++ b/android/app/src/main/java/dev/dettmer/simplenotes/SimpleNotesApplication.kt
@@ -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()
+ }
+}
diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/sync/NetworkMonitor.kt b/android/app/src/main/java/dev/dettmer/simplenotes/sync/NetworkMonitor.kt
new file mode 100644
index 0000000..6e08775
--- /dev/null
+++ b/android/app/src/main/java/dev/dettmer/simplenotes/sync/NetworkMonitor.kt
@@ -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()
+ .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")
+ }
+}
diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/sync/SyncWorker.kt b/android/app/src/main/java/dev/dettmer/simplenotes/sync/SyncWorker.kt
index 7965527..397f0af 100644
--- a/android/app/src/main/java/dev/dettmer/simplenotes/sync/SyncWorker.kt
+++ b/android/app/src/main/java/dev/dettmer/simplenotes/sync/SyncWorker.kt
@@ -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()
}
}
}
diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/utils/NotificationHelper.kt b/android/app/src/main/java/dev/dettmer/simplenotes/utils/NotificationHelper.kt
index 20e82d8..0f7de98 100644
--- a/android/app/src/main/java/dev/dettmer/simplenotes/utils/NotificationHelper.kt
+++ b/android/app/src/main/java/dev/dettmer/simplenotes/utils/NotificationHelper.kt
@@ -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)
+ }
}
diff --git a/android/gradlew b/android/gradlew
old mode 100644
new mode 100755