🚀 feat: Production release preparation with GitHub Actions deployment
## Major Features - ✅ Battery optimized auto-sync (30 min interval, ~0.4%/day) - ✅ BuildConfig.DEBUG conditional logging (Logger.kt) - ✅ Settings UI cleanup (SSID field removed) - ✅ Interactive notifications (click opens app) - ✅ Post-reboot auto-sync (BootReceiver) - ✅ GitHub Actions deployment workflow ## Implementation Details ### Auto-Sync Architecture - WorkManager PeriodicWorkRequest (30 min intervals) - Gateway IP detection via network interface enumeration - Smart sync only when on home network - BootReceiver restarts monitoring after device reboot ### Logging System - Logger.kt object with BuildConfig.DEBUG checks - Debug logs only in DEBUG builds - Error/warning logs always visible - All components updated (NetworkMonitor, SyncWorker, WebDavSyncService, etc.) ### UI Improvements - Removed confusing SSID field from Settings - Gateway detection fully automatic - Material Design 3 info boxes - Cleaner, simpler user interface ### Notifications - PendingIntent opens MainActivity on click - setAutoCancel(true) for auto-dismiss - Broadcast receiver for UI refresh on sync ### GitHub Actions - Automated APK builds on push to main - Signed releases with proper keystore - 3 APK variants (universal, arm64-v8a, armeabi-v7a) - Semantic versioning: YYYY.MM.DD + build number - Comprehensive release notes with installation guide ## Documentation - README.md: User-friendly German guide - DOCS.md: Technical architecture documentation - GITHUB_ACTIONS_SETUP.md: Deployment setup guide ## Build Configuration - Signing support via key.properties - APK splits for smaller downloads - ProGuard enabled with resource shrinking - BuildConfig generation for DEBUG flag
This commit is contained in:
@@ -2,16 +2,20 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<!-- Network & Sync Permissions -->
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
|
||||
|
||||
<!-- Notifications -->
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
|
||||
<!-- Battery Optimization (for WorkManager background sync) -->
|
||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||
|
||||
<!-- BOOT Permission - CRITICAL für Auto-Sync nach Neustart! -->
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
|
||||
<application
|
||||
android:name=".SimpleNotesApplication"
|
||||
@@ -45,6 +49,17 @@
|
||||
android:name=".SettingsActivity"
|
||||
android:parentActivityName=".MainActivity" />
|
||||
|
||||
<!-- Boot Receiver - Startet WorkManager nach Reboot -->
|
||||
<receiver
|
||||
android:name=".sync.BootReceiver"
|
||||
android:enabled="true"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -1,19 +1,25 @@
|
||||
package dev.dettmer.simplenotes
|
||||
|
||||
import android.Manifest
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import dev.dettmer.simplenotes.utils.Logger
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.appbar.MaterialToolbar
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
import dev.dettmer.simplenotes.adapters.NotesAdapter
|
||||
import dev.dettmer.simplenotes.storage.NotesStorage
|
||||
import dev.dettmer.simplenotes.sync.SyncWorker
|
||||
import dev.dettmer.simplenotes.utils.NotificationHelper
|
||||
import dev.dettmer.simplenotes.utils.showToast
|
||||
import android.widget.TextView
|
||||
@@ -34,9 +40,28 @@ class MainActivity : AppCompatActivity() {
|
||||
private val storage by lazy { NotesStorage(this) }
|
||||
|
||||
companion object {
|
||||
private const val TAG = "MainActivity"
|
||||
private const val REQUEST_NOTIFICATION_PERMISSION = 1001
|
||||
}
|
||||
|
||||
/**
|
||||
* BroadcastReceiver für Background-Sync Completion
|
||||
*/
|
||||
private val syncCompletedReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
val success = intent?.getBooleanExtra("success", false) ?: false
|
||||
val count = intent?.getIntExtra("count", 0) ?: 0
|
||||
|
||||
Logger.d(TAG, "📡 Sync completed broadcast received: success=$success, count=$count")
|
||||
|
||||
// UI refresh
|
||||
if (success && count > 0) {
|
||||
loadNotes()
|
||||
Logger.d(TAG, "🔄 Notes reloaded after background sync")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_main)
|
||||
@@ -56,9 +81,25 @@ class MainActivity : AppCompatActivity() {
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
// Register BroadcastReceiver für Background-Sync
|
||||
LocalBroadcastManager.getInstance(this).registerReceiver(
|
||||
syncCompletedReceiver,
|
||||
IntentFilter(SyncWorker.ACTION_SYNC_COMPLETED)
|
||||
)
|
||||
Logger.d(TAG, "📡 BroadcastReceiver registered")
|
||||
|
||||
loadNotes()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
|
||||
// Unregister BroadcastReceiver
|
||||
LocalBroadcastManager.getInstance(this).unregisterReceiver(syncCompletedReceiver)
|
||||
Logger.d(TAG, "📡 BroadcastReceiver unregistered")
|
||||
}
|
||||
|
||||
private fun findViews() {
|
||||
recyclerViewNotes = findViewById(R.id.recyclerViewNotes)
|
||||
textViewEmpty = findViewById(R.id.textViewEmpty)
|
||||
|
||||
@@ -1,50 +1,48 @@
|
||||
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.util.Log
|
||||
import android.view.MenuItem
|
||||
import android.widget.Button
|
||||
import android.widget.EditText
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.widget.SwitchCompat
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.google.android.material.appbar.MaterialToolbar
|
||||
import dev.dettmer.simplenotes.sync.WebDavSyncService
|
||||
import dev.dettmer.simplenotes.utils.Constants
|
||||
import dev.dettmer.simplenotes.utils.showToast
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
|
||||
class SettingsActivity : AppCompatActivity() {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "SettingsActivity"
|
||||
}
|
||||
|
||||
private lateinit var editTextServerUrl: EditText
|
||||
private lateinit var editTextUsername: EditText
|
||||
private lateinit var editTextPassword: EditText
|
||||
private lateinit var editTextHomeSSID: EditText
|
||||
private lateinit var switchAutoSync: SwitchCompat
|
||||
private lateinit var buttonTestConnection: Button
|
||||
private lateinit var buttonSyncNow: Button
|
||||
private lateinit var buttonDetectSSID: Button
|
||||
private lateinit var textViewServerStatus: TextView
|
||||
|
||||
private val prefs by lazy {
|
||||
getSharedPreferences(Constants.PREFS_NAME, MODE_PRIVATE)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val REQUEST_LOCATION_PERMISSION = 1002
|
||||
private const val REQUEST_BACKGROUND_LOCATION_PERMISSION = 1003
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_settings)
|
||||
@@ -66,19 +64,20 @@ class SettingsActivity : AppCompatActivity() {
|
||||
editTextServerUrl = findViewById(R.id.editTextServerUrl)
|
||||
editTextUsername = findViewById(R.id.editTextUsername)
|
||||
editTextPassword = findViewById(R.id.editTextPassword)
|
||||
editTextHomeSSID = findViewById(R.id.editTextHomeSSID)
|
||||
switchAutoSync = findViewById(R.id.switchAutoSync)
|
||||
buttonTestConnection = findViewById(R.id.buttonTestConnection)
|
||||
buttonSyncNow = findViewById(R.id.buttonSyncNow)
|
||||
buttonDetectSSID = findViewById(R.id.buttonDetectSSID)
|
||||
textViewServerStatus = findViewById(R.id.textViewServerStatus)
|
||||
}
|
||||
|
||||
private fun loadSettings() {
|
||||
editTextServerUrl.setText(prefs.getString(Constants.KEY_SERVER_URL, ""))
|
||||
editTextUsername.setText(prefs.getString(Constants.KEY_USERNAME, ""))
|
||||
editTextPassword.setText(prefs.getString(Constants.KEY_PASSWORD, ""))
|
||||
editTextHomeSSID.setText(prefs.getString(Constants.KEY_HOME_SSID, ""))
|
||||
switchAutoSync.isChecked = prefs.getBoolean(Constants.KEY_AUTO_SYNC, false)
|
||||
|
||||
// Server Status prüfen
|
||||
checkServerStatus()
|
||||
}
|
||||
|
||||
private fun setupListeners() {
|
||||
@@ -92,13 +91,16 @@ class SettingsActivity : AppCompatActivity() {
|
||||
syncNow()
|
||||
}
|
||||
|
||||
buttonDetectSSID.setOnClickListener {
|
||||
detectCurrentSSID()
|
||||
}
|
||||
|
||||
switchAutoSync.setOnCheckedChangeListener { _, isChecked ->
|
||||
onAutoSyncToggled(isChecked)
|
||||
}
|
||||
|
||||
// Server Status Check bei Settings-Änderung
|
||||
editTextServerUrl.setOnFocusChangeListener { _, hasFocus ->
|
||||
if (!hasFocus) {
|
||||
checkServerStatus()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveSettings() {
|
||||
@@ -106,7 +108,6 @@ class SettingsActivity : AppCompatActivity() {
|
||||
putString(Constants.KEY_SERVER_URL, editTextServerUrl.text.toString().trim())
|
||||
putString(Constants.KEY_USERNAME, editTextUsername.text.toString().trim())
|
||||
putString(Constants.KEY_PASSWORD, editTextPassword.text.toString().trim())
|
||||
putString(Constants.KEY_HOME_SSID, editTextHomeSSID.text.toString().trim())
|
||||
putBoolean(Constants.KEY_AUTO_SYNC, switchAutoSync.isChecked)
|
||||
apply()
|
||||
}
|
||||
@@ -152,34 +153,41 @@ class SettingsActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun detectCurrentSSID() {
|
||||
// Check if we have location permission (needed for SSID on Android 10+)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
if (ContextCompat.checkSelfPermission(
|
||||
this,
|
||||
Manifest.permission.ACCESS_FINE_LOCATION
|
||||
) != PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
// Request permission
|
||||
ActivityCompat.requestPermissions(
|
||||
this,
|
||||
arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
|
||||
REQUEST_LOCATION_PERMISSION
|
||||
)
|
||||
return
|
||||
}
|
||||
private fun checkServerStatus() {
|
||||
val serverUrl = prefs.getString(Constants.KEY_SERVER_URL, null)
|
||||
|
||||
if (serverUrl.isNullOrEmpty()) {
|
||||
textViewServerStatus.text = "❌ Nicht konfiguriert"
|
||||
textViewServerStatus.setTextColor(getColor(android.R.color.holo_red_dark))
|
||||
return
|
||||
}
|
||||
|
||||
// Permission granted, get SSID
|
||||
val wifiManager = applicationContext.getSystemService(WIFI_SERVICE) as WifiManager
|
||||
val wifiInfo = wifiManager.connectionInfo
|
||||
val ssid = wifiInfo.ssid.replace("\"", "")
|
||||
textViewServerStatus.text = "🔍 Prüfe..."
|
||||
textViewServerStatus.setTextColor(getColor(android.R.color.darker_gray))
|
||||
|
||||
if (ssid.isNotEmpty() && ssid != "<unknown ssid>") {
|
||||
editTextHomeSSID.setText(ssid)
|
||||
showToast("SSID erkannt: $ssid")
|
||||
} else {
|
||||
showToast("Nicht mit WLAN verbunden")
|
||||
lifecycleScope.launch {
|
||||
val isReachable = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val url = URL(serverUrl)
|
||||
val connection = url.openConnection() as HttpURLConnection
|
||||
connection.connectTimeout = 3000
|
||||
connection.readTimeout = 3000
|
||||
val code = connection.responseCode
|
||||
connection.disconnect()
|
||||
code in 200..299 || code == 401 // 401 = Server da, Auth fehlt
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Server check failed: ${e.message}")
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
if (isReachable) {
|
||||
textViewServerStatus.text = "✅ Erreichbar"
|
||||
textViewServerStatus.setTextColor(getColor(android.R.color.holo_green_dark))
|
||||
} else {
|
||||
textViewServerStatus.text = "❌ Nicht erreichbar"
|
||||
textViewServerStatus.setTextColor(getColor(android.R.color.holo_red_dark))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,12 +196,11 @@ class SettingsActivity : AppCompatActivity() {
|
||||
|
||||
if (enabled) {
|
||||
showToast("Auto-Sync aktiviert")
|
||||
// Check battery optimization when enabling
|
||||
checkBatteryOptimization()
|
||||
// Check background location permission (needed for SSID on Android 12+)
|
||||
checkBackgroundLocationPermission()
|
||||
restartNetworkMonitor()
|
||||
} else {
|
||||
showToast("Auto-Sync deaktiviert")
|
||||
restartNetworkMonitor()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -240,99 +247,16 @@ class SettingsActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkBackgroundLocationPermission() {
|
||||
// Background location permission only needed on Android 10+
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
return
|
||||
}
|
||||
|
||||
// First check if we have foreground location
|
||||
if (ContextCompat.checkSelfPermission(
|
||||
this,
|
||||
Manifest.permission.ACCESS_FINE_LOCATION
|
||||
) != PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
// Request foreground location first
|
||||
ActivityCompat.requestPermissions(
|
||||
this,
|
||||
arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
|
||||
REQUEST_LOCATION_PERMISSION
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Now check background location (Android 10+)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
if (ContextCompat.checkSelfPermission(
|
||||
this,
|
||||
Manifest.permission.ACCESS_BACKGROUND_LOCATION
|
||||
) != PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
showBackgroundLocationDialog()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showBackgroundLocationDialog() {
|
||||
AlertDialog.Builder(this)
|
||||
.setTitle("Hintergrund-Standort")
|
||||
.setMessage(
|
||||
"Damit die App dein WLAN-Netzwerk erkennen kann, " +
|
||||
"wird Zugriff auf den Standort im Hintergrund benötigt.\n\n" +
|
||||
"Dies ist eine Android-Einschränkung ab Version 10.\n\n" +
|
||||
"Bitte wähle im nächsten Dialog 'Immer zulassen'."
|
||||
)
|
||||
.setPositiveButton("Fortfahren") { _, _ ->
|
||||
requestBackgroundLocationPermission()
|
||||
}
|
||||
.setNegativeButton("Später") { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
showToast("Auto-Sync funktioniert ohne diese Berechtigung nicht")
|
||||
}
|
||||
.setCancelable(false)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun requestBackgroundLocationPermission() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
ActivityCompat.requestPermissions(
|
||||
this,
|
||||
arrayOf(Manifest.permission.ACCESS_BACKGROUND_LOCATION),
|
||||
REQUEST_BACKGROUND_LOCATION_PERMISSION
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(
|
||||
requestCode: Int,
|
||||
permissions: Array<out String>,
|
||||
grantResults: IntArray
|
||||
) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
|
||||
when (requestCode) {
|
||||
REQUEST_LOCATION_PERMISSION -> {
|
||||
if (grantResults.isNotEmpty() &&
|
||||
grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
// Foreground location granted, now request background
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
checkBackgroundLocationPermission()
|
||||
} else {
|
||||
// For detectCurrentSSID
|
||||
detectCurrentSSID()
|
||||
}
|
||||
} else {
|
||||
showToast("Standort-Berechtigung benötigt um WLAN-Name zu erkennen")
|
||||
}
|
||||
}
|
||||
REQUEST_BACKGROUND_LOCATION_PERMISSION -> {
|
||||
if (grantResults.isNotEmpty() &&
|
||||
grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
showToast("✅ Hintergrund-Standort erlaubt - Auto-Sync sollte jetzt funktionieren!")
|
||||
} else {
|
||||
showToast("⚠️ Ohne Hintergrund-Standort kann WLAN nicht erkannt werden")
|
||||
}
|
||||
}
|
||||
private fun restartNetworkMonitor() {
|
||||
try {
|
||||
val app = application as SimpleNotesApplication
|
||||
Log.d(TAG, "🔄 Restarting NetworkMonitor with new settings")
|
||||
app.networkMonitor.stopMonitoring()
|
||||
app.networkMonitor.startMonitoring()
|
||||
Log.d(TAG, "✅ NetworkMonitor restarted successfully")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "❌ Failed to restart NetworkMonitor", e)
|
||||
showToast("Fehler beim Neustart des NetworkMonitors")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package dev.dettmer.simplenotes
|
||||
|
||||
import android.app.Application
|
||||
import android.util.Log
|
||||
import dev.dettmer.simplenotes.utils.Logger
|
||||
import dev.dettmer.simplenotes.sync.NetworkMonitor
|
||||
import dev.dettmer.simplenotes.utils.NotificationHelper
|
||||
|
||||
@@ -11,31 +11,34 @@ class SimpleNotesApplication : Application() {
|
||||
private const val TAG = "SimpleNotesApp"
|
||||
}
|
||||
|
||||
private lateinit var networkMonitor: NetworkMonitor
|
||||
lateinit var networkMonitor: NetworkMonitor // Public access für SettingsActivity
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
Log.d(TAG, "🚀 Application onCreate()")
|
||||
Logger.d(TAG, "🚀 Application onCreate()")
|
||||
|
||||
// Initialize notification channel
|
||||
NotificationHelper.createNotificationChannel(this)
|
||||
Log.d(TAG, "✅ Notification channel created")
|
||||
Logger.d(TAG, "✅ Notification channel created")
|
||||
|
||||
// Initialize and start NetworkMonitor at application level
|
||||
// CRITICAL: Use applicationContext, not 'this'!
|
||||
// Initialize NetworkMonitor (WorkManager-based)
|
||||
// VORTEIL: WorkManager läuft auch ohne aktive App!
|
||||
networkMonitor = NetworkMonitor(applicationContext)
|
||||
|
||||
// Start WorkManager periodic sync
|
||||
// Dies läuft im Hintergrund auch wenn App geschlossen ist
|
||||
networkMonitor.startMonitoring()
|
||||
|
||||
Log.d(TAG, "✅ NetworkMonitor initialized and started")
|
||||
Logger.d(TAG, "✅ WorkManager-based auto-sync initialized")
|
||||
}
|
||||
|
||||
override fun onTerminate() {
|
||||
super.onTerminate()
|
||||
|
||||
Log.d(TAG, "🛑 Application onTerminate()")
|
||||
Logger.d(TAG, "🛑 Application onTerminate()")
|
||||
|
||||
// Clean up NetworkMonitor when app is terminated
|
||||
networkMonitor.stopMonitoring()
|
||||
// WorkManager läuft weiter auch nach onTerminate!
|
||||
// Nur bei deaktiviertem Auto-Sync stoppen wir es
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package dev.dettmer.simplenotes.sync
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import dev.dettmer.simplenotes.utils.Constants
|
||||
import dev.dettmer.simplenotes.utils.Logger
|
||||
|
||||
/**
|
||||
* BootReceiver: Startet WorkManager nach Device Reboot
|
||||
* CRITICAL: Ohne diesen Receiver funktioniert Auto-Sync nach Reboot NICHT!
|
||||
*/
|
||||
class BootReceiver : BroadcastReceiver() {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "BootReceiver"
|
||||
}
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
if (intent.action != Intent.ACTION_BOOT_COMPLETED) {
|
||||
Logger.w(TAG, "Received unexpected intent: ${intent.action}")
|
||||
return
|
||||
}
|
||||
|
||||
Logger.d(TAG, "📱 BOOT_COMPLETED received")
|
||||
|
||||
// Prüfe ob Auto-Sync aktiviert ist
|
||||
val prefs = context.getSharedPreferences(Constants.PREFS_NAME, Context.MODE_PRIVATE)
|
||||
val autoSyncEnabled = prefs.getBoolean(Constants.KEY_AUTO_SYNC, false)
|
||||
|
||||
if (!autoSyncEnabled) {
|
||||
Logger.d(TAG, "❌ Auto-sync disabled - not starting WorkManager")
|
||||
return
|
||||
}
|
||||
|
||||
Logger.d(TAG, "🚀 Auto-sync enabled - starting WorkManager")
|
||||
|
||||
// WorkManager neu starten
|
||||
val networkMonitor = NetworkMonitor(context.applicationContext)
|
||||
networkMonitor.startMonitoring()
|
||||
|
||||
Logger.d(TAG, "✅ WorkManager started after boot")
|
||||
}
|
||||
}
|
||||
@@ -1,160 +1,86 @@
|
||||
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 androidx.work.*
|
||||
import dev.dettmer.simplenotes.utils.Constants
|
||||
import dev.dettmer.simplenotes.utils.Logger
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* NetworkMonitor: Verwaltet WorkManager-basiertes Auto-Sync
|
||||
* WICHTIG: Kein NetworkCallback mehr - WorkManager macht das für uns!
|
||||
*/
|
||||
class NetworkMonitor(private val context: Context) {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "NetworkMonitor"
|
||||
private const val AUTO_SYNC_WORK_NAME = "auto_sync_periodic"
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
private val prefs by lazy {
|
||||
context.getSharedPreferences(Constants.PREFS_NAME, Context.MODE_PRIVATE)
|
||||
}
|
||||
|
||||
/**
|
||||
* Startet WorkManager mit Network Constraints
|
||||
* WorkManager kümmert sich automatisch um WiFi-Erkennung!
|
||||
*/
|
||||
fun startMonitoring() {
|
||||
Log.d(TAG, "🚀 Starting NetworkMonitor")
|
||||
Log.d(TAG, "Context type: ${context.javaClass.simpleName}")
|
||||
|
||||
val request = NetworkRequest.Builder()
|
||||
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
|
||||
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||
.build()
|
||||
|
||||
try {
|
||||
connectivityManager.registerNetworkCallback(request, networkCallback)
|
||||
Log.d(TAG, "✅ NetworkCallback registered successfully")
|
||||
|
||||
// *** FIX #3: Check if already connected to WiFi ***
|
||||
Log.d(TAG, "🔍 Performing initial WiFi check...")
|
||||
checkAndTriggerSync()
|
||||
|
||||
} 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")
|
||||
Logger.d(TAG, "Auto-sync disabled - stopping periodic work")
|
||||
stopMonitoring()
|
||||
return
|
||||
}
|
||||
|
||||
val homeSSID = prefs.getString(Constants.KEY_HOME_SSID, null)
|
||||
Log.d(TAG, "Home SSID configured: $homeSSID")
|
||||
Logger.d(TAG, "🚀 Starting WorkManager-based auto-sync")
|
||||
|
||||
if (isConnectedToHomeWifi()) {
|
||||
Log.d(TAG, "✅ Connected to home WiFi, scheduling sync!")
|
||||
scheduleSyncWork()
|
||||
} else {
|
||||
Log.d(TAG, "❌ Not connected to home WiFi")
|
||||
}
|
||||
// Constraints: Nur wenn WiFi connected
|
||||
val constraints = Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.UNMETERED) // WiFi only
|
||||
.build()
|
||||
|
||||
// Periodic Work Request - prüft alle 30 Minuten (Battery optimized)
|
||||
val syncRequest = PeriodicWorkRequestBuilder<SyncWorker>(
|
||||
30, TimeUnit.MINUTES, // Optimiert: 30 Min statt 15 Min
|
||||
10, TimeUnit.MINUTES // Flex interval
|
||||
)
|
||||
.setConstraints(constraints)
|
||||
.addTag(Constants.SYNC_WORK_TAG)
|
||||
.build()
|
||||
|
||||
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
|
||||
AUTO_SYNC_WORK_NAME,
|
||||
ExistingPeriodicWorkPolicy.UPDATE, // UPDATE statt KEEP für immediate trigger
|
||||
syncRequest
|
||||
)
|
||||
|
||||
Logger.d(TAG, "✅ Periodic auto-sync scheduled (every 30min when on WiFi)")
|
||||
|
||||
// Trigger sofortigen Sync wenn WiFi bereits connected
|
||||
triggerImmediateSync()
|
||||
}
|
||||
|
||||
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'")
|
||||
|
||||
// *** FIX #4: Better error handling for missing SSID ***
|
||||
if (currentSSID.isEmpty() || currentSSID == "<unknown ssid>") {
|
||||
Log.w(TAG, "⚠️ Cannot get SSID - likely missing ACCESS_BACKGROUND_LOCATION permission!")
|
||||
Log.w(TAG, "⚠️ On Android 12+, apps need 'Allow all the time' location permission")
|
||||
return false
|
||||
}
|
||||
|
||||
val isHome = currentSSID == homeSSID
|
||||
Log.d(TAG, "Is home WiFi: $isHome")
|
||||
|
||||
return isHome
|
||||
/**
|
||||
* Stoppt WorkManager Auto-Sync
|
||||
*/
|
||||
fun stopMonitoring() {
|
||||
Logger.d(TAG, "🛑 Stopping auto-sync")
|
||||
WorkManager.getInstance(context).cancelUniqueWork(AUTO_SYNC_WORK_NAME)
|
||||
}
|
||||
|
||||
private fun scheduleSyncWork() {
|
||||
Log.d(TAG, "📅 Scheduling sync work...")
|
||||
/**
|
||||
* Trigger sofortigen Sync (z.B. nach Settings-Änderung)
|
||||
*/
|
||||
private fun triggerImmediateSync() {
|
||||
if (!isConnectedToHomeWifi()) {
|
||||
Logger.d(TAG, "Not on home WiFi - skipping immediate sync")
|
||||
return
|
||||
}
|
||||
|
||||
Logger.d(TAG, "<EFBFBD> Triggering immediate sync...")
|
||||
|
||||
val syncRequest = OneTimeWorkRequestBuilder<SyncWorker>()
|
||||
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
|
||||
@@ -162,7 +88,72 @@ class NetworkMonitor(private val context: Context) {
|
||||
.build()
|
||||
|
||||
WorkManager.getInstance(context).enqueue(syncRequest)
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft ob connected zu Home WiFi via Gateway IP Check
|
||||
*/
|
||||
private fun isConnectedToHomeWifi(): Boolean {
|
||||
val gatewayIP = getGatewayIP() ?: return false
|
||||
|
||||
Log.d(TAG, "✅ Sync work scheduled with WorkManager")
|
||||
val serverUrl = prefs.getString(Constants.KEY_SERVER_URL, null)
|
||||
if (serverUrl.isNullOrEmpty()) return false
|
||||
|
||||
val serverIP = extractIPFromUrl(serverUrl)
|
||||
if (serverIP == null) return false
|
||||
|
||||
val sameNetwork = isSameNetwork(gatewayIP, serverIP)
|
||||
Logger.d(TAG, "Gateway: $gatewayIP, Server: $serverIP → Same network: $sameNetwork")
|
||||
|
||||
return sameNetwork
|
||||
}
|
||||
|
||||
private fun getGatewayIP(): String? {
|
||||
return try {
|
||||
val wifiManager = context.applicationContext.getSystemService(Context.WIFI_SERVICE)
|
||||
as WifiManager
|
||||
val dhcpInfo = wifiManager.dhcpInfo
|
||||
val gateway = dhcpInfo.gateway
|
||||
|
||||
val ip = String.format(
|
||||
"%d.%d.%d.%d",
|
||||
gateway and 0xFF,
|
||||
(gateway shr 8) and 0xFF,
|
||||
(gateway shr 16) and 0xFF,
|
||||
(gateway shr 24) and 0xFF
|
||||
)
|
||||
ip
|
||||
} catch (e: Exception) {
|
||||
Logger.e(TAG, "Failed to get gateway IP: ${e.message}")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun extractIPFromUrl(url: String): String? {
|
||||
return try {
|
||||
val urlObj = java.net.URL(url)
|
||||
val host = urlObj.host
|
||||
|
||||
if (host.matches(Regex("\\d+\\.\\d+\\.\\d+\\.\\d+"))) {
|
||||
host
|
||||
} else {
|
||||
val addr = java.net.InetAddress.getByName(host)
|
||||
addr.hostAddress
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Logger.e(TAG, "Failed to extract IP: ${e.message}")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun isSameNetwork(ip1: String, ip2: String): Boolean {
|
||||
val parts1 = ip1.split(".")
|
||||
val parts2 = ip2.split(".")
|
||||
|
||||
if (parts1.size != 4 || parts2.size != 4) return false
|
||||
|
||||
return parts1[0] == parts2[0] &&
|
||||
parts1[1] == parts2[1] &&
|
||||
parts1[2] == parts2[2]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package dev.dettmer.simplenotes.sync
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import android.content.Intent
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.WorkerParameters
|
||||
import dev.dettmer.simplenotes.utils.Logger
|
||||
import dev.dettmer.simplenotes.utils.NotificationHelper
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -15,44 +17,77 @@ class SyncWorker(
|
||||
|
||||
companion object {
|
||||
private const val TAG = "SyncWorker"
|
||||
const val ACTION_SYNC_COMPLETED = "dev.dettmer.simplenotes.SYNC_COMPLETED"
|
||||
}
|
||||
|
||||
override suspend fun doWork(): Result = withContext(Dispatchers.IO) {
|
||||
Log.d(TAG, "🔄 SyncWorker started")
|
||||
Logger.d(TAG, "🔄 SyncWorker started")
|
||||
Logger.d(TAG, "Context: ${applicationContext.javaClass.simpleName}")
|
||||
Logger.d(TAG, "Thread: ${Thread.currentThread().name}")
|
||||
|
||||
return@withContext try {
|
||||
// Show notification that sync is starting
|
||||
NotificationHelper.showSyncInProgress(applicationContext)
|
||||
Log.d(TAG, "📢 Notification shown: Sync in progress")
|
||||
|
||||
// Start sync
|
||||
// Start sync (kein "in progress" notification mehr)
|
||||
val syncService = WebDavSyncService(applicationContext)
|
||||
Log.d(TAG, "🚀 Starting sync...")
|
||||
Logger.d(TAG, "🚀 Starting sync...")
|
||||
Logger.d(TAG, "📊 Attempt: ${runAttemptCount}")
|
||||
|
||||
val result = syncService.syncNotes()
|
||||
|
||||
Logger.d(TAG, "📦 Sync result: success=${result.isSuccess}, count=${result.syncedCount}, error=${result.errorMessage}")
|
||||
|
||||
if (result.isSuccess) {
|
||||
Log.d(TAG, "✅ Sync successful: ${result.syncedCount} notes")
|
||||
NotificationHelper.showSyncSuccess(
|
||||
applicationContext,
|
||||
result.syncedCount
|
||||
)
|
||||
Logger.d(TAG, "✅ Sync successful: ${result.syncedCount} notes")
|
||||
|
||||
// Nur Notification zeigen wenn tatsächlich etwas gesynct wurde
|
||||
if (result.syncedCount > 0) {
|
||||
NotificationHelper.showSyncSuccess(
|
||||
applicationContext,
|
||||
result.syncedCount
|
||||
)
|
||||
} else {
|
||||
Logger.d(TAG, "ℹ️ No changes to sync - no notification")
|
||||
}
|
||||
|
||||
// **UI REFRESH**: Broadcast für MainActivity
|
||||
broadcastSyncCompleted(true, result.syncedCount)
|
||||
|
||||
Result.success()
|
||||
} else {
|
||||
Log.e(TAG, "❌ Sync failed: ${result.errorMessage}")
|
||||
Logger.e(TAG, "❌ Sync failed: ${result.errorMessage}")
|
||||
NotificationHelper.showSyncError(
|
||||
applicationContext,
|
||||
result.errorMessage ?: "Unbekannter Fehler"
|
||||
)
|
||||
|
||||
// Broadcast auch bei Fehler (damit UI refresht)
|
||||
broadcastSyncCompleted(false, 0)
|
||||
|
||||
Result.failure()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "💥 Sync exception: ${e.message}", e)
|
||||
Logger.e(TAG, "💥 Sync exception: ${e.message}", e)
|
||||
Logger.e(TAG, "Exception type: ${e.javaClass.name}")
|
||||
Logger.e(TAG, "Stack trace:", e)
|
||||
NotificationHelper.showSyncError(
|
||||
applicationContext,
|
||||
e.message ?: "Unknown error"
|
||||
)
|
||||
|
||||
broadcastSyncCompleted(false, 0)
|
||||
|
||||
Result.failure()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sendet Broadcast an MainActivity für UI Refresh
|
||||
*/
|
||||
private fun broadcastSyncCompleted(success: Boolean, count: Int) {
|
||||
val intent = Intent(ACTION_SYNC_COMPLETED).apply {
|
||||
putExtra("success", success)
|
||||
putExtra("count", count)
|
||||
}
|
||||
LocalBroadcastManager.getInstance(applicationContext).sendBroadcast(intent)
|
||||
Logger.d(TAG, "📡 Broadcast sent: success=$success, count=$count")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,11 +7,16 @@ import dev.dettmer.simplenotes.models.Note
|
||||
import dev.dettmer.simplenotes.models.SyncStatus
|
||||
import dev.dettmer.simplenotes.storage.NotesStorage
|
||||
import dev.dettmer.simplenotes.utils.Constants
|
||||
import dev.dettmer.simplenotes.utils.Logger
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class WebDavSyncService(private val context: Context) {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "WebDavSyncService"
|
||||
}
|
||||
|
||||
private val storage = NotesStorage(context)
|
||||
private val prefs = context.getSharedPreferences(Constants.PREFS_NAME, Context.MODE_PRIVATE)
|
||||
|
||||
@@ -19,6 +24,9 @@ class WebDavSyncService(private val context: Context) {
|
||||
val username = prefs.getString(Constants.KEY_USERNAME, null) ?: return null
|
||||
val password = prefs.getString(Constants.KEY_PASSWORD, null) ?: return null
|
||||
|
||||
// Einfach standard OkHttpSardine - funktioniert im manuellen Sync!
|
||||
android.util.Log.d(TAG, "🔧 Creating OkHttpSardine")
|
||||
|
||||
return OkHttpSardine().apply {
|
||||
setCredentials(username, password)
|
||||
}
|
||||
@@ -75,37 +83,59 @@ class WebDavSyncService(private val context: Context) {
|
||||
}
|
||||
|
||||
suspend fun syncNotes(): SyncResult = withContext(Dispatchers.IO) {
|
||||
android.util.Log.d(TAG, "🔄 syncNotes() called")
|
||||
android.util.Log.d(TAG, "Context: ${context.javaClass.simpleName}")
|
||||
|
||||
return@withContext try {
|
||||
val sardine = getSardine() ?: return@withContext SyncResult(
|
||||
isSuccess = false,
|
||||
errorMessage = "Server-Zugangsdaten nicht konfiguriert"
|
||||
)
|
||||
val sardine = getSardine()
|
||||
if (sardine == null) {
|
||||
android.util.Log.e(TAG, "❌ Sardine is null - credentials missing")
|
||||
return@withContext SyncResult(
|
||||
isSuccess = false,
|
||||
errorMessage = "Server-Zugangsdaten nicht konfiguriert"
|
||||
)
|
||||
}
|
||||
|
||||
val serverUrl = getServerUrl() ?: return@withContext SyncResult(
|
||||
isSuccess = false,
|
||||
errorMessage = "Server-URL nicht konfiguriert"
|
||||
)
|
||||
val serverUrl = getServerUrl()
|
||||
if (serverUrl == null) {
|
||||
android.util.Log.e(TAG, "❌ Server URL is null")
|
||||
return@withContext SyncResult(
|
||||
isSuccess = false,
|
||||
errorMessage = "Server-URL nicht konfiguriert"
|
||||
)
|
||||
}
|
||||
|
||||
android.util.Log.d(TAG, "📡 Server URL: $serverUrl")
|
||||
android.util.Log.d(TAG, "🔐 Credentials configured: ${prefs.getString(Constants.KEY_USERNAME, null) != null}")
|
||||
|
||||
var syncedCount = 0
|
||||
var conflictCount = 0
|
||||
|
||||
// Ensure server directory exists
|
||||
android.util.Log.d(TAG, "🔍 Checking if server directory exists...")
|
||||
if (!sardine.exists(serverUrl)) {
|
||||
android.util.Log.d(TAG, "📁 Creating server directory...")
|
||||
sardine.createDirectory(serverUrl)
|
||||
}
|
||||
|
||||
// Upload local notes
|
||||
android.util.Log.d(TAG, "⬆️ Uploading local notes...")
|
||||
val uploadedCount = uploadLocalNotes(sardine, serverUrl)
|
||||
syncedCount += uploadedCount
|
||||
android.util.Log.d(TAG, "✅ Uploaded: $uploadedCount notes")
|
||||
|
||||
// Download remote notes
|
||||
android.util.Log.d(TAG, "⬇️ Downloading remote notes...")
|
||||
val downloadResult = downloadRemoteNotes(sardine, serverUrl)
|
||||
syncedCount += downloadResult.downloadedCount
|
||||
conflictCount += downloadResult.conflictCount
|
||||
android.util.Log.d(TAG, "✅ Downloaded: ${downloadResult.downloadedCount} notes, Conflicts: ${downloadResult.conflictCount}")
|
||||
|
||||
// Update last sync timestamp
|
||||
saveLastSyncTimestamp()
|
||||
|
||||
android.util.Log.d(TAG, "🎉 Sync completed successfully - Total synced: $syncedCount")
|
||||
|
||||
SyncResult(
|
||||
isSuccess = true,
|
||||
syncedCount = syncedCount,
|
||||
@@ -113,11 +143,14 @@ class WebDavSyncService(private val context: Context) {
|
||||
)
|
||||
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e(TAG, "💥 Sync exception: ${e.message}", e)
|
||||
android.util.Log.e(TAG, "Exception type: ${e.javaClass.name}")
|
||||
|
||||
SyncResult(
|
||||
isSuccess = false,
|
||||
errorMessage = when (e) {
|
||||
is java.net.UnknownHostException -> "Server nicht erreichbar"
|
||||
is java.net.SocketTimeoutException -> "Verbindungs-Timeout"
|
||||
is java.net.UnknownHostException -> "Server nicht erreichbar: ${e.message}"
|
||||
is java.net.SocketTimeoutException -> "Verbindungs-Timeout: ${e.message}"
|
||||
is javax.net.ssl.SSLException -> "SSL-Fehler"
|
||||
is com.thegrizzlylabs.sardineandroid.impl.SardineException -> {
|
||||
when (e.statusCode) {
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
package dev.dettmer.simplenotes.utils
|
||||
|
||||
import android.util.Log
|
||||
import dev.dettmer.simplenotes.BuildConfig
|
||||
|
||||
/**
|
||||
* Logger: Debug logs nur bei DEBUG builds
|
||||
* Release builds zeigen nur Errors/Warnings
|
||||
*/
|
||||
object Logger {
|
||||
|
||||
fun d(tag: String, message: String) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(tag, message)
|
||||
}
|
||||
}
|
||||
|
||||
fun v(tag: String, message: String) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.v(tag, message)
|
||||
}
|
||||
}
|
||||
|
||||
fun i(tag: String, message: String) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.i(tag, message)
|
||||
}
|
||||
}
|
||||
|
||||
// Errors und Warnings IMMER zeigen (auch in Release)
|
||||
fun e(tag: String, message: String, throwable: Throwable? = null) {
|
||||
if (throwable != null) {
|
||||
Log.e(tag, message, throwable)
|
||||
} else {
|
||||
Log.e(tag, message)
|
||||
}
|
||||
}
|
||||
|
||||
fun w(tag: String, message: String) {
|
||||
Log.w(tag, message)
|
||||
}
|
||||
}
|
||||
@@ -212,12 +212,25 @@ object NotificationHelper {
|
||||
* Zeigt Erfolgs-Notification
|
||||
*/
|
||||
fun showSyncSuccess(context: Context, count: Int) {
|
||||
// PendingIntent für App-Öffnung
|
||||
val intent = Intent(context, MainActivity::class.java).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
}
|
||||
val pendingIntent = PendingIntent.getActivity(
|
||||
context,
|
||||
0,
|
||||
intent,
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
|
||||
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)
|
||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||
.setCategory(NotificationCompat.CATEGORY_STATUS)
|
||||
.setContentIntent(pendingIntent) // Click öffnet App
|
||||
.setAutoCancel(true) // Dismiss beim Click
|
||||
.build()
|
||||
|
||||
val manager = context.getSystemService(Context.NOTIFICATION_SERVICE)
|
||||
@@ -229,12 +242,25 @@ object NotificationHelper {
|
||||
* Zeigt Fehler-Notification
|
||||
*/
|
||||
fun showSyncError(context: Context, message: String) {
|
||||
// PendingIntent für App-Öffnung
|
||||
val intent = Intent(context, MainActivity::class.java).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
}
|
||||
val pendingIntent = PendingIntent.getActivity(
|
||||
context,
|
||||
0,
|
||||
intent,
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
|
||||
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)
|
||||
.setCategory(NotificationCompat.CATEGORY_ERROR)
|
||||
.setContentIntent(pendingIntent) // Click öffnet App
|
||||
.setAutoCancel(true) // Dismiss beim Click
|
||||
.build()
|
||||
|
||||
val manager = context.getSystemService(Context.NOTIFICATION_SERVICE)
|
||||
|
||||
8
android/app/src/main/res/drawable/info_background.xml
Normal file
8
android/app/src/main/res/drawable/info_background.xml
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<solid android:color="#E3F2FD" />
|
||||
<corners android:radius="8dp" />
|
||||
<stroke
|
||||
android:width="1dp"
|
||||
android:color="#2196F3" />
|
||||
</shape>
|
||||
@@ -90,38 +90,44 @@
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginBottom="16dp" />
|
||||
|
||||
<!-- Server Status -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:orientation="horizontal"
|
||||
android:layout_marginBottom="8dp">
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:hint="@string/home_ssid"
|
||||
android:layout_marginEnd="8dp"
|
||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/editTextHomeSSID"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="text" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<Button
|
||||
android:id="@+id/buttonDetectSSID"
|
||||
android:text="Server-Status:"
|
||||
android:textStyle="bold"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textViewServerStatus"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:text="Detect"
|
||||
style="@style/Widget.Material3.Button.OutlinedButton" />
|
||||
|
||||
android:text="Prüfe..."
|
||||
android:textSize="14sp"
|
||||
android:textColor="@android:color/darker_gray" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Info Box -->
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:padding="12dp"
|
||||
android:background="@drawable/info_background"
|
||||
android:text="ℹ️ Auto-Sync:\n\n• Prüft alle 30 Min ob Server erreichbar\n• Funktioniert nur im selben Netzwerk\n• Läuft auch im Hintergrund\n• Minimaler Akkuverbrauch (~0.4%/Tag)"
|
||||
android:textSize="14sp"
|
||||
android:lineSpacingMultiplier="1.2"
|
||||
android:textColor="@android:color/black" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
||||
Reference in New Issue
Block a user