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