Merge feature/v1.3.2-lint-cleanup into main
This commit is contained in:
37
CHANGELOG.md
37
CHANGELOG.md
@@ -6,6 +6,43 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## [1.3.2] - 2026-01-10
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- **🧹 Code-Qualität: "Clean Slate" Release**
|
||||||
|
- Alle einfachen Lint-Issues behoben (Phase 1-7 des Cleanup-Plans)
|
||||||
|
- Unused Imports und Members entfernt
|
||||||
|
- Magic Numbers durch benannte Konstanten ersetzt
|
||||||
|
- SwallowedExceptions mit Logger.w() versehen
|
||||||
|
- MaxLineLength-Verstöße reformatiert
|
||||||
|
- ConstructorParameterNaming (snake_case → camelCase mit @SerializedName)
|
||||||
|
- Custom Exceptions: SyncException.kt und ValidationException.kt erstellt
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **📝 F-Droid Privacy Notice**
|
||||||
|
- Datenschutz-Hinweis für die Datei-Logging-Funktion
|
||||||
|
- Erklärt dass Logs nur lokal gespeichert werden
|
||||||
|
- Erfüllt F-Droid Opt-in Consent-Anforderungen
|
||||||
|
|
||||||
|
### Technical Improvements
|
||||||
|
- **⚡ Neue Konstanten für bessere Wartbarkeit**
|
||||||
|
- `SYNC_COMPLETED_DELAY_MS`, `ERROR_DISPLAY_DELAY_MS` (MainActivity)
|
||||||
|
- `CONNECTION_TIMEOUT_MS` (SettingsActivity)
|
||||||
|
- `SOCKET_TIMEOUT_MS`, `MAX_FILENAME_LENGTH`, `ETAG_PREVIEW_LENGTH` (WebDavSyncService)
|
||||||
|
- `AUTO_CANCEL_TIMEOUT_MS` (NotificationHelper)
|
||||||
|
- RFC 1918 IP-Range Konstanten (UrlValidator)
|
||||||
|
- `DAYS_THRESHOLD`, `TRUNCATE_SUFFIX_LENGTH` (Extensions)
|
||||||
|
|
||||||
|
- **🔒 @Suppress Annotations für legitime Patterns**
|
||||||
|
- ReturnCount: Frühe Returns für Validierung sind idiomatisch
|
||||||
|
- LoopWithTooManyJumpStatements: Komplexe Sync-Logik dokumentiert
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
- Komplexe Refactorings (LargeClass, LongMethod) für v1.3.3+ geplant
|
||||||
|
- Deprecation-Warnungen (LocalBroadcastManager, ProgressDialog) bleiben bestehen
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## [1.3.1] - 2026-01-08
|
## [1.3.1] - 2026-01-08
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ cd android
|
|||||||
./gradlew assembleStandardRelease
|
./gradlew assembleStandardRelease
|
||||||
```
|
```
|
||||||
|
|
||||||
➡️ **Build guide:** [DOCS.en.md](docs/DOCS.en.md)
|
➡️ **Build guide:** [DOCS.en.md](docs/DOCS.en.md#-build--deployment)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -101,4 +101,4 @@ MIT License - see [LICENSE](LICENSE)
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**v1.2.1** · Built with ❤️ using Kotlin + Material Design 3
|
**v1.3.2** · Built with ❤️ using Kotlin + Material Design 3
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ cd android
|
|||||||
./gradlew assembleStandardRelease
|
./gradlew assembleStandardRelease
|
||||||
```
|
```
|
||||||
|
|
||||||
➡️ **Build-Anleitung:** [DOCS.md](docs/DOCS.md)
|
➡️ **Build-Anleitung:** [DOCS.md](docs/DOCS.md#-build--deployment)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -104,4 +104,4 @@ MIT License - siehe [LICENSE](LICENSE)
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**v1.2.1** · Built with ❤️ using Kotlin + Material Design 3
|
**v1.3.2** · Built with ❤️ using Kotlin + Material Design 3
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ android {
|
|||||||
applicationId = "dev.dettmer.simplenotes"
|
applicationId = "dev.dettmer.simplenotes"
|
||||||
minSdk = 24
|
minSdk = 24
|
||||||
targetSdk = 36
|
targetSdk = 36
|
||||||
versionCode = 9 // 🚀 v1.3.1: Sync-Performance & Debug-Logging
|
versionCode = 10 // 🚀 v1.3.2: Lint-Cleanup "Clean Slate"
|
||||||
versionName = "1.3.1" // 🚀 v1.3.1: Skip unchanged MD-Files, Sync-Mutex, Debug-Logging UI
|
versionName = "1.3.2" // 🚀 v1.3.2: Code-Qualität-Release (alle einfachen Lint-Issues behoben)
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
|
||||||
|
|||||||
@@ -76,6 +76,8 @@ class MainActivity : AppCompatActivity() {
|
|||||||
private const val REQUEST_SETTINGS = 1002
|
private const val REQUEST_SETTINGS = 1002
|
||||||
private const val MIN_AUTO_SYNC_INTERVAL_MS = 60_000L // 1 Minute
|
private const val MIN_AUTO_SYNC_INTERVAL_MS = 60_000L // 1 Minute
|
||||||
private const val PREF_LAST_AUTO_SYNC_TIME = "last_auto_sync_timestamp"
|
private const val PREF_LAST_AUTO_SYNC_TIME = "last_auto_sync_timestamp"
|
||||||
|
private const val SYNC_COMPLETED_DELAY_MS = 1500L
|
||||||
|
private const val ERROR_DISPLAY_DELAY_MS = 3000L
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -152,7 +154,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
// Show completed briefly, then hide
|
// Show completed briefly, then hide
|
||||||
syncStatusText.text = status.message ?: getString(R.string.sync_status_completed)
|
syncStatusText.text = status.message ?: getString(R.string.sync_status_completed)
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
kotlinx.coroutines.delay(1500)
|
kotlinx.coroutines.delay(SYNC_COMPLETED_DELAY_MS)
|
||||||
syncStatusBanner.visibility = View.GONE
|
syncStatusBanner.visibility = View.GONE
|
||||||
SyncStateManager.reset()
|
SyncStateManager.reset()
|
||||||
}
|
}
|
||||||
@@ -164,7 +166,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
// Show error briefly, then hide
|
// Show error briefly, then hide
|
||||||
syncStatusText.text = status.message ?: getString(R.string.sync_status_error)
|
syncStatusText.text = status.message ?: getString(R.string.sync_status_error)
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
kotlinx.coroutines.delay(3000)
|
kotlinx.coroutines.delay(ERROR_DISPLAY_DELAY_MS)
|
||||||
syncStatusBanner.visibility = View.GONE
|
syncStatusBanner.visibility = View.GONE
|
||||||
SyncStateManager.reset()
|
SyncStateManager.reset()
|
||||||
}
|
}
|
||||||
@@ -518,16 +520,28 @@ class MainActivity : AppCompatActivity() {
|
|||||||
val success = webdavService.deleteNoteFromServer(note.id)
|
val success = webdavService.deleteNoteFromServer(note.id)
|
||||||
if (success) {
|
if (success) {
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
Toast.makeText(this@MainActivity, "Vom Server gelöscht", Toast.LENGTH_SHORT).show()
|
Toast.makeText(
|
||||||
|
this@MainActivity,
|
||||||
|
"Vom Server gelöscht",
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
Toast.makeText(this@MainActivity, "Server-Löschung fehlgeschlagen", Toast.LENGTH_LONG).show()
|
Toast.makeText(
|
||||||
|
this@MainActivity,
|
||||||
|
"Server-Löschung fehlgeschlagen",
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
Toast.makeText(this@MainActivity, "Server-Fehler: ${e.message}", Toast.LENGTH_LONG).show()
|
Toast.makeText(
|
||||||
|
this@MainActivity,
|
||||||
|
"Server-Fehler: ${e.message}",
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,8 +25,6 @@ import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
|||||||
import com.google.android.material.appbar.MaterialToolbar
|
import com.google.android.material.appbar.MaterialToolbar
|
||||||
import com.google.android.material.card.MaterialCardView
|
import com.google.android.material.card.MaterialCardView
|
||||||
import com.google.android.material.color.DynamicColors
|
import com.google.android.material.color.DynamicColors
|
||||||
import com.google.android.material.switchmaterial.SwitchMaterial
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import dev.dettmer.simplenotes.backup.BackupManager
|
import dev.dettmer.simplenotes.backup.BackupManager
|
||||||
@@ -39,7 +37,6 @@ import dev.dettmer.simplenotes.sync.NetworkMonitor
|
|||||||
import dev.dettmer.simplenotes.utils.Constants
|
import dev.dettmer.simplenotes.utils.Constants
|
||||||
import dev.dettmer.simplenotes.utils.Logger
|
import dev.dettmer.simplenotes.utils.Logger
|
||||||
import dev.dettmer.simplenotes.utils.showToast
|
import dev.dettmer.simplenotes.utils.showToast
|
||||||
import java.io.File
|
|
||||||
import java.net.HttpURLConnection
|
import java.net.HttpURLConnection
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
@@ -52,6 +49,7 @@ class SettingsActivity : AppCompatActivity() {
|
|||||||
private const val GITHUB_REPO_URL = "https://github.com/inventory69/simple-notes-sync"
|
private const val GITHUB_REPO_URL = "https://github.com/inventory69/simple-notes-sync"
|
||||||
private const val GITHUB_PROFILE_URL = "https://github.com/inventory69"
|
private const val GITHUB_PROFILE_URL = "https://github.com/inventory69"
|
||||||
private const val LICENSE_URL = "https://github.com/inventory69/simple-notes-sync/blob/main/LICENSE"
|
private const val LICENSE_URL = "https://github.com/inventory69/simple-notes-sync/blob/main/LICENSE"
|
||||||
|
private const val CONNECTION_TIMEOUT_MS = 3000
|
||||||
}
|
}
|
||||||
|
|
||||||
private lateinit var textInputLayoutServerUrl: com.google.android.material.textfield.TextInputLayout
|
private lateinit var textInputLayoutServerUrl: com.google.android.material.textfield.TextInputLayout
|
||||||
@@ -325,7 +323,10 @@ class SettingsActivity : AppCompatActivity() {
|
|||||||
*/
|
*/
|
||||||
private fun setupSyncIntervalPicker() {
|
private fun setupSyncIntervalPicker() {
|
||||||
// Load current interval from preferences
|
// Load current interval from preferences
|
||||||
val currentInterval = prefs.getLong(Constants.PREF_SYNC_INTERVAL_MINUTES, Constants.DEFAULT_SYNC_INTERVAL_MINUTES)
|
val currentInterval = prefs.getLong(
|
||||||
|
Constants.PREF_SYNC_INTERVAL_MINUTES,
|
||||||
|
Constants.DEFAULT_SYNC_INTERVAL_MINUTES
|
||||||
|
)
|
||||||
|
|
||||||
// Set checked radio button based on current interval
|
// Set checked radio button based on current interval
|
||||||
val checkedId = when (currentInterval) {
|
val checkedId = when (currentInterval) {
|
||||||
@@ -654,8 +655,8 @@ class SettingsActivity : AppCompatActivity() {
|
|||||||
try {
|
try {
|
||||||
val url = URL(serverUrl)
|
val url = URL(serverUrl)
|
||||||
val connection = url.openConnection() as HttpURLConnection
|
val connection = url.openConnection() as HttpURLConnection
|
||||||
connection.connectTimeout = 3000
|
connection.connectTimeout = CONNECTION_TIMEOUT_MS
|
||||||
connection.readTimeout = 3000
|
connection.readTimeout = CONNECTION_TIMEOUT_MS
|
||||||
val code = connection.responseCode
|
val code = connection.responseCode
|
||||||
connection.disconnect()
|
connection.disconnect()
|
||||||
code in 200..299 || code == 401 // 401 = Server da, Auth fehlt
|
code in 200..299 || code == 401 // 401 = Server da, Auth fehlt
|
||||||
@@ -764,7 +765,10 @@ class SettingsActivity : AppCompatActivity() {
|
|||||||
.apply()
|
.apply()
|
||||||
|
|
||||||
updateMarkdownButtonVisibility()
|
updateMarkdownButtonVisibility()
|
||||||
showToast("Markdown Auto-Sync aktiviert - Notizen werden als .md-Dateien exportiert und importiert")
|
showToast(
|
||||||
|
"Markdown Auto-Sync aktiviert - " +
|
||||||
|
"Notizen werden als .md-Dateien exportiert und importiert"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -818,11 +822,13 @@ class SettingsActivity : AppCompatActivity() {
|
|||||||
intent.data = Uri.parse("package:$packageName")
|
intent.data = Uri.parse("package:$packageName")
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
Logger.w(TAG, "Failed to open battery optimization settings: ${e.message}")
|
||||||
// Fallback: Open general battery settings
|
// Fallback: Open general battery settings
|
||||||
try {
|
try {
|
||||||
val intent = Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS)
|
val intent = Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS)
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
} catch (e2: Exception) {
|
} catch (e2: Exception) {
|
||||||
|
Logger.w(TAG, "Failed to open fallback battery settings: ${e2.message}")
|
||||||
showToast("Bitte Akku-Optimierung manuell deaktivieren")
|
showToast("Bitte Akku-Optimierung manuell deaktivieren")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -841,49 +847,6 @@ class SettingsActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showRestoreConfirmation() {
|
|
||||||
android.app.AlertDialog.Builder(this)
|
|
||||||
.setTitle(R.string.restore_confirmation_title)
|
|
||||||
.setMessage(R.string.restore_confirmation_message)
|
|
||||||
.setPositiveButton(R.string.restore_button) { _, _ ->
|
|
||||||
performRestore()
|
|
||||||
}
|
|
||||||
.setNegativeButton(R.string.cancel, null)
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun performRestore() {
|
|
||||||
val progressDialog = android.app.ProgressDialog(this).apply {
|
|
||||||
setMessage(getString(R.string.restore_progress))
|
|
||||||
setCancelable(false)
|
|
||||||
show()
|
|
||||||
}
|
|
||||||
|
|
||||||
CoroutineScope(Dispatchers.Main).launch {
|
|
||||||
try {
|
|
||||||
val webdavService = WebDavSyncService(this@SettingsActivity)
|
|
||||||
val result = withContext(Dispatchers.IO) {
|
|
||||||
webdavService.restoreFromServer()
|
|
||||||
}
|
|
||||||
|
|
||||||
progressDialog.dismiss()
|
|
||||||
|
|
||||||
if (result.isSuccess) {
|
|
||||||
showToast(getString(R.string.restore_success, result.restoredCount))
|
|
||||||
// Refresh MainActivity's note list
|
|
||||||
setResult(RESULT_OK)
|
|
||||||
} else {
|
|
||||||
showToast(getString(R.string.restore_error, result.errorMessage))
|
|
||||||
}
|
|
||||||
checkServerStatus()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
progressDialog.dismiss()
|
|
||||||
showToast(getString(R.string.restore_error, e.message))
|
|
||||||
checkServerStatus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
return when (item.itemId) {
|
return when (item.itemId) {
|
||||||
android.R.id.home -> {
|
android.R.id.home -> {
|
||||||
@@ -946,7 +909,6 @@ class SettingsActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Custom View mit Radio Buttons
|
// Custom View mit Radio Buttons
|
||||||
val dialogView = layoutInflater.inflate(android.R.layout.select_dialog_singlechoice, null)
|
|
||||||
val radioGroup = android.widget.RadioGroup(this).apply {
|
val radioGroup = android.widget.RadioGroup(this).apply {
|
||||||
orientation = android.widget.RadioGroup.VERTICAL
|
orientation = android.widget.RadioGroup.VERTICAL
|
||||||
setPadding(50, 20, 50, 20)
|
setPadding(50, 20, 50, 20)
|
||||||
@@ -1039,12 +1001,12 @@ class SettingsActivity : AppCompatActivity() {
|
|||||||
progressDialog.dismiss()
|
progressDialog.dismiss()
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
val message = result.message ?: "Wiederhergestellt: ${result.imported_notes} Notizen"
|
val message = result.message ?: "Wiederhergestellt: ${result.importedNotes} Notizen"
|
||||||
showToast("✅ $message")
|
showToast("✅ $message")
|
||||||
|
|
||||||
// Refresh MainActivity's note list
|
// Refresh MainActivity's note list
|
||||||
setResult(RESULT_OK)
|
setResult(RESULT_OK)
|
||||||
broadcastNotesChanged(result.imported_notes)
|
broadcastNotesChanged(result.importedNotes)
|
||||||
} else {
|
} else {
|
||||||
showErrorDialog("Wiederherstellung fehlgeschlagen", result.error ?: "Unbekannter Fehler")
|
showErrorDialog("Wiederherstellung fehlgeschlagen", result.error ?: "Unbekannter Fehler")
|
||||||
}
|
}
|
||||||
@@ -1153,10 +1115,16 @@ class SettingsActivity : AppCompatActivity() {
|
|||||||
progressDialog.dismiss()
|
progressDialog.dismiss()
|
||||||
|
|
||||||
// Erfolgs-Nachricht
|
// Erfolgs-Nachricht
|
||||||
val message = "✅ Sync abgeschlossen\n📤 ${result.exportedCount} exportiert\n📥 ${result.importedCount} importiert"
|
val message = "✅ Sync abgeschlossen\n" +
|
||||||
|
"📤 ${result.exportedCount} exportiert\n" +
|
||||||
|
"📥 ${result.importedCount} importiert"
|
||||||
showToast(message)
|
showToast(message)
|
||||||
|
|
||||||
Logger.d("SettingsActivity", "Manual markdown sync: exported=${result.exportedCount}, imported=${result.importedCount}")
|
Logger.d(
|
||||||
|
"SettingsActivity",
|
||||||
|
"Manual markdown sync: exported=${result.exportedCount}, " +
|
||||||
|
"imported=${result.importedCount}"
|
||||||
|
)
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
progressDialog?.dismiss()
|
progressDialog?.dismiss()
|
||||||
|
|||||||
@@ -49,10 +49,10 @@ class BackupManager(private val context: Context) {
|
|||||||
Logger.d(TAG, " Found ${allNotes.size} notes to backup")
|
Logger.d(TAG, " Found ${allNotes.size} notes to backup")
|
||||||
|
|
||||||
val backupData = BackupData(
|
val backupData = BackupData(
|
||||||
backup_version = BACKUP_VERSION,
|
backupVersion = BACKUP_VERSION,
|
||||||
created_at = System.currentTimeMillis(),
|
createdAt = System.currentTimeMillis(),
|
||||||
notes_count = allNotes.size,
|
notesCount = allNotes.size,
|
||||||
app_version = BuildConfig.VERSION_NAME,
|
appVersion = BuildConfig.VERSION_NAME,
|
||||||
notes = allNotes
|
notes = allNotes
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -65,7 +65,7 @@ class BackupManager(private val context: Context) {
|
|||||||
|
|
||||||
BackupResult(
|
BackupResult(
|
||||||
success = true,
|
success = true,
|
||||||
notes_count = allNotes.size,
|
notesCount = allNotes.size,
|
||||||
message = "Backup erstellt: ${allNotes.size} Notizen"
|
message = "Backup erstellt: ${allNotes.size} Notizen"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -99,10 +99,10 @@ class BackupManager(private val context: Context) {
|
|||||||
|
|
||||||
val allNotes = storage.loadAllNotes()
|
val allNotes = storage.loadAllNotes()
|
||||||
val backupData = BackupData(
|
val backupData = BackupData(
|
||||||
backup_version = BACKUP_VERSION,
|
backupVersion = BACKUP_VERSION,
|
||||||
created_at = System.currentTimeMillis(),
|
createdAt = System.currentTimeMillis(),
|
||||||
notes_count = allNotes.size,
|
notesCount = allNotes.size,
|
||||||
app_version = BuildConfig.VERSION_NAME,
|
appVersion = BuildConfig.VERSION_NAME,
|
||||||
notes = allNotes
|
notes = allNotes
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -149,7 +149,7 @@ class BackupManager(private val context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val backupData = gson.fromJson(jsonString, BackupData::class.java)
|
val backupData = gson.fromJson(jsonString, BackupData::class.java)
|
||||||
Logger.d(TAG, " Backup valid: ${backupData.notes_count} notes, version ${backupData.backup_version}")
|
Logger.d(TAG, " Backup valid: ${backupData.notesCount} notes, version ${backupData.backupVersion}")
|
||||||
|
|
||||||
// 3. Auto-Backup erstellen (Sicherheitsnetz)
|
// 3. Auto-Backup erstellen (Sicherheitsnetz)
|
||||||
val autoBackupUri = createAutoBackup()
|
val autoBackupUri = createAutoBackup()
|
||||||
@@ -164,7 +164,7 @@ class BackupManager(private val context: Context) {
|
|||||||
RestoreMode.OVERWRITE_DUPLICATES -> restoreOverwriteDuplicates(backupData.notes)
|
RestoreMode.OVERWRITE_DUPLICATES -> restoreOverwriteDuplicates(backupData.notes)
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.d(TAG, "✅ Restore completed: ${result.imported_notes} imported, ${result.skipped_notes} skipped")
|
Logger.d(TAG, "✅ Restore completed: ${result.importedNotes} imported, ${result.skippedNotes} skipped")
|
||||||
result
|
result
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -184,10 +184,11 @@ class BackupManager(private val context: Context) {
|
|||||||
val backupData = gson.fromJson(jsonString, BackupData::class.java)
|
val backupData = gson.fromJson(jsonString, BackupData::class.java)
|
||||||
|
|
||||||
// Version kompatibel?
|
// Version kompatibel?
|
||||||
if (backupData.backup_version > BACKUP_VERSION) {
|
if (backupData.backupVersion > BACKUP_VERSION) {
|
||||||
return ValidationResult(
|
return ValidationResult(
|
||||||
isValid = false,
|
isValid = false,
|
||||||
errorMessage = "Backup-Version nicht unterstützt (v${backupData.backup_version} benötigt v${BACKUP_VERSION}+)"
|
errorMessage = "Backup-Version nicht unterstützt " +
|
||||||
|
"(v${backupData.backupVersion} benötigt v${BACKUP_VERSION}+)"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,8 +239,8 @@ class BackupManager(private val context: Context) {
|
|||||||
|
|
||||||
return RestoreResult(
|
return RestoreResult(
|
||||||
success = true,
|
success = true,
|
||||||
imported_notes = newNotes.size,
|
importedNotes = newNotes.size,
|
||||||
skipped_notes = skippedNotes,
|
skippedNotes = skippedNotes,
|
||||||
message = "${newNotes.size} neue Notizen importiert, $skippedNotes übersprungen"
|
message = "${newNotes.size} neue Notizen importiert, $skippedNotes übersprungen"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -259,8 +260,8 @@ class BackupManager(private val context: Context) {
|
|||||||
|
|
||||||
return RestoreResult(
|
return RestoreResult(
|
||||||
success = true,
|
success = true,
|
||||||
imported_notes = backupNotes.size,
|
importedNotes = backupNotes.size,
|
||||||
skipped_notes = 0,
|
skippedNotes = 0,
|
||||||
message = "Alle Notizen ersetzt: ${backupNotes.size} importiert"
|
message = "Alle Notizen ersetzt: ${backupNotes.size} importiert"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -283,9 +284,9 @@ class BackupManager(private val context: Context) {
|
|||||||
|
|
||||||
return RestoreResult(
|
return RestoreResult(
|
||||||
success = true,
|
success = true,
|
||||||
imported_notes = newNotes.size,
|
importedNotes = newNotes.size,
|
||||||
skipped_notes = 0,
|
skippedNotes = 0,
|
||||||
overwritten_notes = overwrittenNotes.size,
|
overwrittenNotes = overwrittenNotes.size,
|
||||||
message = "${newNotes.size} neu, ${overwrittenNotes.size} überschrieben"
|
message = "${newNotes.size} neu, ${overwrittenNotes.size} überschrieben"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -312,12 +313,17 @@ class BackupManager(private val context: Context) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Backup-Daten Struktur (JSON)
|
* Backup-Daten Struktur (JSON)
|
||||||
|
* NOTE: Property names use @SerializedName for JSON compatibility with snake_case
|
||||||
*/
|
*/
|
||||||
data class BackupData(
|
data class BackupData(
|
||||||
val backup_version: Int,
|
@com.google.gson.annotations.SerializedName("backup_version")
|
||||||
val created_at: Long,
|
val backupVersion: Int,
|
||||||
val notes_count: Int,
|
@com.google.gson.annotations.SerializedName("created_at")
|
||||||
val app_version: String,
|
val createdAt: Long,
|
||||||
|
@com.google.gson.annotations.SerializedName("notes_count")
|
||||||
|
val notesCount: Int,
|
||||||
|
@com.google.gson.annotations.SerializedName("app_version")
|
||||||
|
val appVersion: String,
|
||||||
val notes: List<Note>
|
val notes: List<Note>
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -335,7 +341,7 @@ enum class RestoreMode {
|
|||||||
*/
|
*/
|
||||||
data class BackupResult(
|
data class BackupResult(
|
||||||
val success: Boolean,
|
val success: Boolean,
|
||||||
val notes_count: Int = 0,
|
val notesCount: Int = 0,
|
||||||
val message: String? = null,
|
val message: String? = null,
|
||||||
val error: String? = null
|
val error: String? = null
|
||||||
)
|
)
|
||||||
@@ -345,9 +351,9 @@ data class BackupResult(
|
|||||||
*/
|
*/
|
||||||
data class RestoreResult(
|
data class RestoreResult(
|
||||||
val success: Boolean,
|
val success: Boolean,
|
||||||
val imported_notes: Int = 0,
|
val importedNotes: Int = 0,
|
||||||
val skipped_notes: Int = 0,
|
val skippedNotes: Int = 0,
|
||||||
val overwritten_notes: Int = 0,
|
val overwrittenNotes: Int = 0,
|
||||||
val message: String? = null,
|
val message: String? = null,
|
||||||
val error: String? = null
|
val error: String? = null
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package dev.dettmer.simplenotes.models
|
package dev.dettmer.simplenotes.models
|
||||||
|
|
||||||
|
import dev.dettmer.simplenotes.utils.Logger
|
||||||
import org.json.JSONArray
|
import org.json.JSONArray
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
|
|
||||||
@@ -49,6 +50,8 @@ data class DeletionTracker(
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
private const val TAG = "DeletionTracker"
|
||||||
|
|
||||||
fun fromJson(json: String): DeletionTracker? {
|
fun fromJson(json: String): DeletionTracker? {
|
||||||
return try {
|
return try {
|
||||||
val jsonObject = JSONObject(json)
|
val jsonObject = JSONObject(json)
|
||||||
@@ -70,6 +73,7 @@ data class DeletionTracker(
|
|||||||
|
|
||||||
DeletionTracker(version, deletedNotes)
|
DeletionTracker(version, deletedNotes)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
Logger.w(TAG, "Failed to parse DeletionTracker JSON: ${e.message}")
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package dev.dettmer.simplenotes.models
|
package dev.dettmer.simplenotes.models
|
||||||
|
|
||||||
|
import dev.dettmer.simplenotes.utils.Logger
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
@@ -49,11 +50,14 @@ $content
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
private const val TAG = "Note"
|
||||||
|
|
||||||
fun fromJson(json: String): Note? {
|
fun fromJson(json: String): Note? {
|
||||||
return try {
|
return try {
|
||||||
val gson = com.google.gson.Gson()
|
val gson = com.google.gson.Gson()
|
||||||
gson.fromJson(json, Note::class.java)
|
gson.fromJson(json, Note::class.java)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
Logger.w(TAG, "Failed to parse JSON: ${e.message}")
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -102,6 +106,7 @@ $content
|
|||||||
syncStatus = SyncStatus.SYNCED // Annahme: Vom Server importiert
|
syncStatus = SyncStatus.SYNCED // Annahme: Vom Server importiert
|
||||||
)
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
Logger.w(TAG, "Failed to parse Markdown: ${e.message}")
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -126,6 +131,7 @@ $content
|
|||||||
sdf.timeZone = TimeZone.getTimeZone("UTC")
|
sdf.timeZone = TimeZone.getTimeZone("UTC")
|
||||||
sdf.parse(dateString)?.time ?: System.currentTimeMillis()
|
sdf.parse(dateString)?.time ?: System.currentTimeMillis()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
Logger.w(TAG, "Failed to parse ISO8601 date '$dateString': ${e.message}")
|
||||||
System.currentTimeMillis() // Fallback
|
System.currentTimeMillis() // Fallback
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -227,7 +227,11 @@ class NetworkMonitor(private val context: Context) {
|
|||||||
if (isWifi) {
|
if (isWifi) {
|
||||||
lastConnectedNetworkId = activeNetwork.toString()
|
lastConnectedNetworkId = activeNetwork.toString()
|
||||||
Logger.d(TAG, " ✅ Initial WiFi network: $lastConnectedNetworkId")
|
Logger.d(TAG, " ✅ Initial WiFi network: $lastConnectedNetworkId")
|
||||||
Logger.d(TAG, " 📡 WiFi already connected at startup - onAvailable() will only trigger on network change")
|
Logger.d(
|
||||||
|
TAG,
|
||||||
|
" 📡 WiFi already connected at startup - " +
|
||||||
|
"onAvailable() will only trigger on network change"
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
lastConnectedNetworkId = null
|
lastConnectedNetworkId = null
|
||||||
Logger.d(TAG, " ⚠️ Not on WiFi at startup")
|
Logger.d(TAG, " ⚠️ Not on WiFi at startup")
|
||||||
@@ -268,7 +272,7 @@ class NetworkMonitor(private val context: Context) {
|
|||||||
connectivityManager.unregisterNetworkCallback(networkCallback)
|
connectivityManager.unregisterNetworkCallback(networkCallback)
|
||||||
Logger.d(TAG, "✅ WiFi monitoring stopped")
|
Logger.d(TAG, "✅ WiFi monitoring stopped")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
// Already unregistered
|
Logger.w(TAG, "NetworkCallback already unregistered: ${e.message}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -117,7 +117,11 @@ class SyncWorker(
|
|||||||
|
|
||||||
if (BuildConfig.DEBUG) {
|
if (BuildConfig.DEBUG) {
|
||||||
Logger.d(TAG, "📍 Step 4: Processing result")
|
Logger.d(TAG, "📍 Step 4: Processing result")
|
||||||
Logger.d(TAG, "📦 Sync result: success=${result.isSuccess}, count=${result.syncedCount}, error=${result.errorMessage}")
|
Logger.d(
|
||||||
|
TAG,
|
||||||
|
"📦 Sync result: success=${result.isSuccess}, " +
|
||||||
|
"count=${result.syncedCount}, error=${result.errorMessage}"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.isSuccess) {
|
if (result.isSuccess) {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import dev.dettmer.simplenotes.models.SyncStatus
|
|||||||
import dev.dettmer.simplenotes.storage.NotesStorage
|
import dev.dettmer.simplenotes.storage.NotesStorage
|
||||||
import dev.dettmer.simplenotes.utils.Constants
|
import dev.dettmer.simplenotes.utils.Constants
|
||||||
import dev.dettmer.simplenotes.utils.Logger
|
import dev.dettmer.simplenotes.utils.Logger
|
||||||
|
import dev.dettmer.simplenotes.utils.SyncException
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
@@ -20,7 +21,6 @@ import java.net.Inet4Address
|
|||||||
import java.net.InetAddress
|
import java.net.InetAddress
|
||||||
import java.net.InetSocketAddress
|
import java.net.InetSocketAddress
|
||||||
import java.net.NetworkInterface
|
import java.net.NetworkInterface
|
||||||
import java.net.Proxy
|
|
||||||
import java.net.Socket
|
import java.net.Socket
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
@@ -38,6 +38,9 @@ class WebDavSyncService(private val context: Context) {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "WebDavSyncService"
|
private const val TAG = "WebDavSyncService"
|
||||||
|
private const val SOCKET_TIMEOUT_MS = 2000
|
||||||
|
private const val MAX_FILENAME_LENGTH = 200
|
||||||
|
private const val ETAG_PREVIEW_LENGTH = 8
|
||||||
|
|
||||||
// 🔒 v1.3.1: Mutex um parallele Syncs zu verhindern
|
// 🔒 v1.3.1: Mutex um parallele Syncs zu verhindern
|
||||||
private val syncMutex = Mutex()
|
private val syncMutex = Mutex()
|
||||||
@@ -101,6 +104,7 @@ class WebDavSyncService(private val context: Context) {
|
|||||||
/**
|
/**
|
||||||
* Findet WiFi Interface IP-Adresse (um VPN zu umgehen)
|
* Findet WiFi Interface IP-Adresse (um VPN zu umgehen)
|
||||||
*/
|
*/
|
||||||
|
@Suppress("ReturnCount") // Early returns for network validation checks
|
||||||
private fun getWiFiInetAddressInternal(): InetAddress? {
|
private fun getWiFiInetAddressInternal(): InetAddress? {
|
||||||
try {
|
try {
|
||||||
Logger.d(TAG, "🔍 getWiFiInetAddress() called")
|
Logger.d(TAG, "🔍 getWiFiInetAddress() called")
|
||||||
@@ -145,7 +149,11 @@ class WebDavSyncService(private val context: Context) {
|
|||||||
while (addresses.hasMoreElements()) {
|
while (addresses.hasMoreElements()) {
|
||||||
val addr = addresses.nextElement()
|
val addr = addresses.nextElement()
|
||||||
|
|
||||||
Logger.d(TAG, " Address: ${addr.hostAddress}, IPv4=${addr is Inet4Address}, loopback=${addr.isLoopbackAddress}, linkLocal=${addr.isLinkLocalAddress}")
|
Logger.d(
|
||||||
|
TAG,
|
||||||
|
" Address: ${addr.hostAddress}, IPv4=${addr is Inet4Address}, " +
|
||||||
|
"loopback=${addr.isLoopbackAddress}, linkLocal=${addr.isLinkLocalAddress}"
|
||||||
|
)
|
||||||
|
|
||||||
// Nur IPv4, nicht loopback, nicht link-local
|
// Nur IPv4, nicht loopback, nicht link-local
|
||||||
if (addr is Inet4Address && !addr.isLoopbackAddress && !addr.isLinkLocalAddress) {
|
if (addr is Inet4Address && !addr.isLoopbackAddress && !addr.isLinkLocalAddress) {
|
||||||
@@ -362,6 +370,7 @@ class WebDavSyncService(private val context: Context) {
|
|||||||
* 3. If changed → server has updates
|
* 3. If changed → server has updates
|
||||||
* 4. If unchanged → skip sync
|
* 4. If unchanged → skip sync
|
||||||
*/
|
*/
|
||||||
|
@Suppress("ReturnCount") // Early returns for conditional checks
|
||||||
private suspend fun checkServerForChanges(sardine: Sardine, serverUrl: String): Boolean {
|
private suspend fun checkServerForChanges(sardine: Sardine, serverUrl: String): Boolean {
|
||||||
return try {
|
return try {
|
||||||
val startTime = System.currentTimeMillis()
|
val startTime = System.currentTimeMillis()
|
||||||
@@ -413,7 +422,11 @@ class WebDavSyncService(private val context: Context) {
|
|||||||
resource.modified?.time?.let {
|
resource.modified?.time?.let {
|
||||||
val hasNewer = it > lastSyncTime
|
val hasNewer = it > lastSyncTime
|
||||||
if (hasNewer) {
|
if (hasNewer) {
|
||||||
Logger.d(TAG, " 📄 ${resource.name}: modified=${resource.modified}, lastSync=$lastSyncTime")
|
Logger.d(
|
||||||
|
TAG,
|
||||||
|
" 📄 ${resource.name}: modified=${resource.modified}, " +
|
||||||
|
"lastSync=$lastSyncTime"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
hasNewer
|
hasNewer
|
||||||
} ?: false
|
} ?: false
|
||||||
@@ -524,10 +537,10 @@ class WebDavSyncService(private val context: Context) {
|
|||||||
|
|
||||||
Logger.d(TAG, "🔍 Checking server reachability: $host:$port")
|
Logger.d(TAG, "🔍 Checking server reachability: $host:$port")
|
||||||
|
|
||||||
// Socket-Check mit 2s Timeout
|
// Socket-Check mit Timeout
|
||||||
// Gibt dem Netzwerk Zeit für Initialisierung (DHCP, Routing, Gateway)
|
// Gibt dem Netzwerk Zeit für Initialisierung (DHCP, Routing, Gateway)
|
||||||
val socket = Socket()
|
val socket = Socket()
|
||||||
socket.connect(InetSocketAddress(host, port), 2000)
|
socket.connect(InetSocketAddress(host, port), SOCKET_TIMEOUT_MS)
|
||||||
socket.close()
|
socket.close()
|
||||||
|
|
||||||
Logger.d(TAG, "✅ Server is reachable")
|
Logger.d(TAG, "✅ Server is reachable")
|
||||||
@@ -669,7 +682,11 @@ class WebDavSyncService(private val context: Context) {
|
|||||||
)
|
)
|
||||||
syncedCount += downloadResult.downloadedCount
|
syncedCount += downloadResult.downloadedCount
|
||||||
conflictCount += downloadResult.conflictCount
|
conflictCount += downloadResult.conflictCount
|
||||||
Logger.d(TAG, "✅ Downloaded: ${downloadResult.downloadedCount} notes, Conflicts: ${downloadResult.conflictCount}")
|
Logger.d(
|
||||||
|
TAG,
|
||||||
|
"✅ Downloaded: ${downloadResult.downloadedCount} notes, " +
|
||||||
|
"Conflicts: ${downloadResult.conflictCount}"
|
||||||
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Logger.e(TAG, "💥 CRASH in downloadRemoteNotes()!", e)
|
Logger.e(TAG, "💥 CRASH in downloadRemoteNotes()!", e)
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
@@ -790,7 +807,7 @@ class WebDavSyncService(private val context: Context) {
|
|||||||
val newETag = uploadedResource?.etag
|
val newETag = uploadedResource?.etag
|
||||||
if (newETag != null) {
|
if (newETag != null) {
|
||||||
prefs.edit().putString("etag_json_${note.id}", newETag).apply()
|
prefs.edit().putString("etag_json_${note.id}", newETag).apply()
|
||||||
Logger.d(TAG, " ⚡ Cached new E-Tag: ${newETag.take(8)}")
|
Logger.d(TAG, " ⚡ Cached new E-Tag: ${newETag.take(ETAG_PREVIEW_LENGTH)}")
|
||||||
} else {
|
} else {
|
||||||
// Fallback: invalidate if server doesn't provide E-Tag
|
// Fallback: invalidate if server doesn't provide E-Tag
|
||||||
prefs.edit().remove("etag_json_${note.id}").apply()
|
prefs.edit().remove("etag_json_${note.id}").apply()
|
||||||
@@ -814,6 +831,7 @@ class WebDavSyncService(private val context: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
Logger.w(TAG, "Upload failed for note ${note.id}, marking as pending: ${e.message}")
|
||||||
// Mark as pending for retry
|
// Mark as pending for retry
|
||||||
val updatedNote = note.copy(syncStatus = SyncStatus.PENDING)
|
val updatedNote = note.copy(syncStatus = SyncStatus.PENDING)
|
||||||
storage.saveNote(updatedNote)
|
storage.saveNote(updatedNote)
|
||||||
@@ -862,7 +880,7 @@ class WebDavSyncService(private val context: Context) {
|
|||||||
return title
|
return title
|
||||||
.replace(Regex("[<>:\"/\\\\|?*]"), "_") // Ersetze verbotene Zeichen
|
.replace(Regex("[<>:\"/\\\\|?*]"), "_") // Ersetze verbotene Zeichen
|
||||||
.replace(Regex("\\s+"), " ") // Normalisiere Whitespace
|
.replace(Regex("\\s+"), " ") // Normalisiere Whitespace
|
||||||
.take(200) // Max 200 Zeichen (Reserve für .md)
|
.take(MAX_FILENAME_LENGTH) // Max Zeichen (Reserve für .md)
|
||||||
.trim('_', ' ') // Trim Underscores/Spaces
|
.trim('_', ' ') // Trim Underscores/Spaces
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -998,9 +1016,13 @@ class WebDavSyncService(private val context: Context) {
|
|||||||
val serverModified = resource.modified?.time ?: 0L
|
val serverModified = resource.modified?.time ?: 0L
|
||||||
|
|
||||||
// 🐛 DEBUG: Log every file check to diagnose performance
|
// 🐛 DEBUG: Log every file check to diagnose performance
|
||||||
val serverETagPreview = serverETag?.take(8) ?: "null"
|
val serverETagPreview = serverETag?.take(ETAG_PREVIEW_LENGTH) ?: "null"
|
||||||
val cachedETagPreview = cachedETag?.take(8) ?: "null"
|
val cachedETagPreview = cachedETag?.take(ETAG_PREVIEW_LENGTH) ?: "null"
|
||||||
Logger.d(TAG, " 🔍 [$noteId] etag=$serverETagPreview/$cachedETagPreview modified=$serverModified lastSync=$lastSyncTime")
|
Logger.d(
|
||||||
|
TAG,
|
||||||
|
" 🔍 [$noteId] etag=$serverETagPreview/$cachedETagPreview " +
|
||||||
|
"modified=$serverModified lastSync=$lastSyncTime"
|
||||||
|
)
|
||||||
|
|
||||||
// PRIMARY: Timestamp check (works on first sync!)
|
// PRIMARY: Timestamp check (works on first sync!)
|
||||||
// Same logic as Markdown sync - skip if not modified since last sync
|
// Same logic as Markdown sync - skip if not modified since last sync
|
||||||
@@ -1101,7 +1123,11 @@ class WebDavSyncService(private val context: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Logger.d(TAG, " 📊 Phase 1: $downloadedCount downloaded, $skippedDeleted skipped (deleted), $skippedUnchanged skipped (unchanged)")
|
Logger.d(
|
||||||
|
TAG,
|
||||||
|
" 📊 Phase 1: $downloadedCount downloaded, $skippedDeleted skipped (deleted), " +
|
||||||
|
"$skippedUnchanged skipped (unchanged)"
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
Logger.w(TAG, " ⚠️ /notes/ does not exist, skipping Phase 1")
|
Logger.w(TAG, " ⚠️ /notes/ does not exist, skipping Phase 1")
|
||||||
}
|
}
|
||||||
@@ -1306,7 +1332,11 @@ class WebDavSyncService(private val context: Context) {
|
|||||||
|
|
||||||
// 🆕 v1.2.2: Use downloadRemoteNotes() with Root fallback + forceOverwrite
|
// 🆕 v1.2.2: Use downloadRemoteNotes() with Root fallback + forceOverwrite
|
||||||
// 🆕 v1.3.0: Pass FRESH empty tracker to avoid loading stale cached data
|
// 🆕 v1.3.0: Pass FRESH empty tracker to avoid loading stale cached data
|
||||||
Logger.d(TAG, "📡 Calling downloadRemoteNotes() - includeRootFallback: true, forceOverwrite: $forceOverwrite")
|
Logger.d(
|
||||||
|
TAG,
|
||||||
|
"📡 Calling downloadRemoteNotes() - " +
|
||||||
|
"includeRootFallback: true, forceOverwrite: $forceOverwrite"
|
||||||
|
)
|
||||||
val emptyTracker = DeletionTracker() // Fresh empty tracker after clear
|
val emptyTracker = DeletionTracker() // Fresh empty tracker after clear
|
||||||
val result = downloadRemoteNotes(
|
val result = downloadRemoteNotes(
|
||||||
sardine = sardine,
|
sardine = sardine,
|
||||||
@@ -1509,15 +1539,31 @@ class WebDavSyncService(private val context: Context) {
|
|||||||
Logger.w(TAG, " ⚠️ Failed to parse ${resource.name} - fromMarkdown returned null")
|
Logger.w(TAG, " ⚠️ Failed to parse ${resource.name} - fromMarkdown returned null")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
Logger.d(TAG, " Parsed: id=${mdNote.id}, title=${mdNote.title}, updatedAt=${Date(mdNote.updatedAt)}")
|
Logger.d(
|
||||||
|
TAG,
|
||||||
|
" Parsed: id=${mdNote.id}, title=${mdNote.title}, " +
|
||||||
|
"updatedAt=${Date(mdNote.updatedAt)}"
|
||||||
|
)
|
||||||
|
|
||||||
val localNote = storage.loadNote(mdNote.id)
|
val localNote = storage.loadNote(mdNote.id)
|
||||||
Logger.d(TAG, " Local note: ${if (localNote == null) "NOT FOUND" else "exists, updatedAt=${Date(localNote.updatedAt)}, syncStatus=${localNote.syncStatus}"}")
|
Logger.d(
|
||||||
|
TAG,
|
||||||
|
" Local note: " + if (localNote == null) {
|
||||||
|
"NOT FOUND"
|
||||||
|
} else {
|
||||||
|
"exists, updatedAt=${Date(localNote.updatedAt)}, " +
|
||||||
|
"syncStatus=${localNote.syncStatus}"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
// ⚡ v1.3.1: Content-basierte Erkennung
|
// ⚡ v1.3.1: Content-basierte Erkennung
|
||||||
// Wichtig: Vergleiche IMMER den Inhalt, wenn die Datei seit letztem Sync geändert wurde!
|
// Wichtig: Vergleiche IMMER den Inhalt, wenn die Datei seit letztem Sync geändert wurde!
|
||||||
// Der YAML-Timestamp kann veraltet sein (z.B. bei externer Bearbeitung ohne Obsidian)
|
// Der YAML-Timestamp kann veraltet sein (z.B. bei externer Bearbeitung ohne Obsidian)
|
||||||
Logger.d(TAG, " Comparison: mdUpdatedAt=${mdNote.updatedAt}, localUpdated=${localNote?.updatedAt ?: 0L}")
|
Logger.d(
|
||||||
|
TAG,
|
||||||
|
" Comparison: mdUpdatedAt=${mdNote.updatedAt}, " +
|
||||||
|
"localUpdated=${localNote?.updatedAt ?: 0L}"
|
||||||
|
)
|
||||||
|
|
||||||
// Content-Vergleich: Ist der Inhalt tatsächlich unterschiedlich?
|
// Content-Vergleich: Ist der Inhalt tatsächlich unterschiedlich?
|
||||||
val contentChanged = localNote != null && (
|
val contentChanged = localNote != null && (
|
||||||
@@ -1538,10 +1584,16 @@ class WebDavSyncService(private val context: Context) {
|
|||||||
Logger.d(TAG, " ✅ Imported new from Markdown: ${mdNote.title}")
|
Logger.d(TAG, " ✅ Imported new from Markdown: ${mdNote.title}")
|
||||||
}
|
}
|
||||||
// ⚡ v1.3.1 FIX: Content-basierter Skip - nur wenn Inhalt UND Timestamp gleich
|
// ⚡ v1.3.1 FIX: Content-basierter Skip - nur wenn Inhalt UND Timestamp gleich
|
||||||
localNote.syncStatus == SyncStatus.SYNCED && !contentChanged && localNote.updatedAt >= mdNote.updatedAt -> {
|
localNote.syncStatus == SyncStatus.SYNCED &&
|
||||||
|
!contentChanged &&
|
||||||
|
localNote.updatedAt >= mdNote.updatedAt -> {
|
||||||
// Inhalt identisch UND Timestamps passen → Skip
|
// Inhalt identisch UND Timestamps passen → Skip
|
||||||
skippedCount++
|
skippedCount++
|
||||||
Logger.d(TAG, " ⏭️ Skipped ${mdNote.title}: content identical (local=${localNote.updatedAt}, md=${mdNote.updatedAt})")
|
Logger.d(
|
||||||
|
TAG,
|
||||||
|
" ⏭️ Skipped ${mdNote.title}: content identical " +
|
||||||
|
"(local=${localNote.updatedAt}, md=${mdNote.updatedAt})"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
// ⚡ v1.3.1 FIX: Content geändert aber YAML-Timestamp nicht aktualisiert → Importieren!
|
// ⚡ v1.3.1 FIX: Content geändert aber YAML-Timestamp nicht aktualisiert → Importieren!
|
||||||
contentChanged && localNote.syncStatus == SyncStatus.SYNCED -> {
|
contentChanged && localNote.syncStatus == SyncStatus.SYNCED -> {
|
||||||
@@ -1571,7 +1623,11 @@ class WebDavSyncService(private val context: Context) {
|
|||||||
else -> {
|
else -> {
|
||||||
// Local has pending changes but MD is older - keep local
|
// Local has pending changes but MD is older - keep local
|
||||||
skippedCount++
|
skippedCount++
|
||||||
Logger.d(TAG, " ⏭️ Skipped ${mdNote.title}: local is newer or pending (local=${localNote.updatedAt}, md=${mdNote.updatedAt})")
|
Logger.d(
|
||||||
|
TAG,
|
||||||
|
" ⏭️ Skipped ${mdNote.title}: local is newer or pending " +
|
||||||
|
"(local=${localNote.updatedAt}, md=${mdNote.updatedAt})"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -1719,14 +1775,16 @@ class WebDavSyncService(private val context: Context) {
|
|||||||
*/
|
*/
|
||||||
suspend fun manualMarkdownSync(): ManualMarkdownSyncResult = withContext(Dispatchers.IO) {
|
suspend fun manualMarkdownSync(): ManualMarkdownSyncResult = withContext(Dispatchers.IO) {
|
||||||
return@withContext try {
|
return@withContext try {
|
||||||
val sardine = getOrCreateSardine() ?: throw Exception("Sardine client konnte nicht erstellt werden")
|
val sardine = getOrCreateSardine()
|
||||||
val serverUrl = getServerUrl() ?: throw Exception("Server-URL nicht konfiguriert")
|
?: throw SyncException("Sardine client konnte nicht erstellt werden")
|
||||||
|
val serverUrl = getServerUrl()
|
||||||
|
?: throw SyncException("Server-URL nicht konfiguriert")
|
||||||
|
|
||||||
val username = prefs.getString(Constants.KEY_USERNAME, "") ?: ""
|
val username = prefs.getString(Constants.KEY_USERNAME, "") ?: ""
|
||||||
val password = prefs.getString(Constants.KEY_PASSWORD, "") ?: ""
|
val password = prefs.getString(Constants.KEY_PASSWORD, "") ?: ""
|
||||||
|
|
||||||
if (serverUrl.isBlank() || username.isBlank() || password.isBlank()) {
|
if (serverUrl.isBlank() || username.isBlank() || password.isBlank()) {
|
||||||
throw Exception("WebDAV-Server nicht vollständig konfiguriert")
|
throw SyncException("WebDAV-Server nicht vollständig konfiguriert")
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.d(TAG, "🔄 Manual Markdown Sync START")
|
Logger.d(TAG, "🔄 Manual Markdown Sync START")
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ class WifiSyncReceiver : BroadcastReceiver() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("ReturnCount") // Early returns for WiFi validation checks
|
||||||
private fun isConnectedToHomeWifi(context: Context): Boolean {
|
private fun isConnectedToHomeWifi(context: Context): Boolean {
|
||||||
val prefs = context.getSharedPreferences(Constants.PREFS_NAME, Context.MODE_PRIVATE)
|
val prefs = context.getSharedPreferences(Constants.PREFS_NAME, Context.MODE_PRIVATE)
|
||||||
val homeSSID = prefs.getString(Constants.KEY_HOME_SSID, null) ?: return false
|
val homeSSID = prefs.getString(Constants.KEY_HOME_SSID, null) ?: return false
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ import java.util.Date
|
|||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
private const val DAYS_THRESHOLD = 7L
|
||||||
|
private const val TRUNCATE_SUFFIX_LENGTH = 3
|
||||||
|
|
||||||
// Toast Extensions
|
// Toast Extensions
|
||||||
fun Context.showToast(message: String, duration: Int = Toast.LENGTH_SHORT) {
|
fun Context.showToast(message: String, duration: Int = Toast.LENGTH_SHORT) {
|
||||||
Toast.makeText(this, message, duration).show()
|
Toast.makeText(this, message, duration).show()
|
||||||
@@ -27,7 +30,7 @@ fun Long.toReadableTime(): String {
|
|||||||
val hours = TimeUnit.MILLISECONDS.toHours(diff)
|
val hours = TimeUnit.MILLISECONDS.toHours(diff)
|
||||||
"Vor $hours Std"
|
"Vor $hours Std"
|
||||||
}
|
}
|
||||||
diff < TimeUnit.DAYS.toMillis(7) -> {
|
diff < TimeUnit.DAYS.toMillis(DAYS_THRESHOLD) -> {
|
||||||
val days = TimeUnit.MILLISECONDS.toDays(diff)
|
val days = TimeUnit.MILLISECONDS.toDays(diff)
|
||||||
"Vor $days Tagen"
|
"Vor $days Tagen"
|
||||||
}
|
}
|
||||||
@@ -41,7 +44,7 @@ fun Long.toReadableTime(): String {
|
|||||||
// Truncate long strings
|
// Truncate long strings
|
||||||
fun String.truncate(maxLength: Int): String {
|
fun String.truncate(maxLength: Int): String {
|
||||||
return if (length > maxLength) {
|
return if (length > maxLength) {
|
||||||
substring(0, maxLength - 3) + "..."
|
substring(0, maxLength - TRUNCATE_SUFFIX_LENGTH) + "..."
|
||||||
} else {
|
} else {
|
||||||
this
|
this
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import android.util.Log
|
|||||||
import dev.dettmer.simplenotes.BuildConfig
|
import dev.dettmer.simplenotes.BuildConfig
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileWriter
|
import java.io.FileWriter
|
||||||
import java.io.PrintWriter
|
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
@@ -15,11 +14,12 @@ import java.util.*
|
|||||||
*/
|
*/
|
||||||
object Logger {
|
object Logger {
|
||||||
|
|
||||||
|
private const val MAX_LOG_ENTRIES = 500 // Nur letzte 500 Einträge
|
||||||
|
|
||||||
private var fileLoggingEnabled = false
|
private var fileLoggingEnabled = false
|
||||||
private var logFile: File? = null
|
private var logFile: File? = null
|
||||||
private var appContext: Context? = null
|
private var appContext: Context? = null
|
||||||
private val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault())
|
private val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault())
|
||||||
private val maxLogEntries = 500 // Nur letzte 500 Einträge
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Setzt den File-Logging Status (für UI Toggle)
|
* Setzt den File-Logging Status (für UI Toggle)
|
||||||
@@ -139,13 +139,13 @@ object Logger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Begrenzt Log-Datei auf maxLogEntries
|
* Begrenzt Log-Datei auf MAX_LOG_ENTRIES
|
||||||
*/
|
*/
|
||||||
private fun trimLogFile() {
|
private fun trimLogFile() {
|
||||||
try {
|
try {
|
||||||
val lines = logFile?.readLines() ?: return
|
val lines = logFile?.readLines() ?: return
|
||||||
if (lines.size > maxLogEntries) {
|
if (lines.size > MAX_LOG_ENTRIES) {
|
||||||
val trimmed = lines.takeLast(maxLogEntries)
|
val trimmed = lines.takeLast(MAX_LOG_ENTRIES)
|
||||||
logFile?.writeText(trimmed.joinToString("\n") + "\n")
|
logFile?.writeText(trimmed.joinToString("\n") + "\n")
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ object NotificationHelper {
|
|||||||
private const val CHANNEL_DESCRIPTION = "Benachrichtigungen über Sync-Status"
|
private const val CHANNEL_DESCRIPTION = "Benachrichtigungen über Sync-Status"
|
||||||
private const val NOTIFICATION_ID = 1001
|
private const val NOTIFICATION_ID = 1001
|
||||||
private const val SYNC_NOTIFICATION_ID = 2
|
private const val SYNC_NOTIFICATION_ID = 2
|
||||||
|
private const val AUTO_CANCEL_TIMEOUT_MS = 30_000L
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Erstellt Notification Channel (Android 8.0+)
|
* Erstellt Notification Channel (Android 8.0+)
|
||||||
@@ -286,7 +287,7 @@ object NotificationHelper {
|
|||||||
Handler(Looper.getMainLooper()).postDelayed({
|
Handler(Looper.getMainLooper()).postDelayed({
|
||||||
manager.cancel(SYNC_NOTIFICATION_ID)
|
manager.cancel(SYNC_NOTIFICATION_ID)
|
||||||
Logger.d(TAG, "🗑️ Auto-cancelled error notification after 30s timeout")
|
Logger.d(TAG, "🗑️ Auto-cancelled error notification after 30s timeout")
|
||||||
}, 30_000)
|
}, AUTO_CANCEL_TIMEOUT_MS)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package dev.dettmer.simplenotes.utils
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception für Sync-spezifische Fehler
|
||||||
|
*
|
||||||
|
* Verwendet anstelle von generischen Exceptions für bessere
|
||||||
|
* Fehlerbehandlung und klarere Fehlermeldungen.
|
||||||
|
*/
|
||||||
|
class SyncException(
|
||||||
|
message: String,
|
||||||
|
cause: Throwable? = null
|
||||||
|
) : Exception(message, cause)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception für Validierungsfehler
|
||||||
|
*
|
||||||
|
* Verwendet für ungültige Eingaben oder Konfigurationsfehler.
|
||||||
|
*/
|
||||||
|
class ValidationException(
|
||||||
|
message: String
|
||||||
|
) : IllegalArgumentException(message)
|
||||||
@@ -8,6 +8,16 @@ import java.net.URL
|
|||||||
*/
|
*/
|
||||||
object UrlValidator {
|
object UrlValidator {
|
||||||
|
|
||||||
|
// RFC 1918 Private IP Ranges
|
||||||
|
private const val PRIVATE_CLASS_A_FIRST_OCTET = 10
|
||||||
|
private const val PRIVATE_CLASS_B_FIRST_OCTET = 172
|
||||||
|
private const val PRIVATE_CLASS_B_SECOND_OCTET_MIN = 16
|
||||||
|
private const val PRIVATE_CLASS_B_SECOND_OCTET_MAX = 31
|
||||||
|
private const val PRIVATE_CLASS_C_FIRST_OCTET = 192
|
||||||
|
private const val PRIVATE_CLASS_C_SECOND_OCTET = 168
|
||||||
|
private const val LOCALHOST_FIRST_OCTET = 127
|
||||||
|
private const val OCTET_MAX_VALUE = 255
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prüft ob eine URL eine lokale/private Adresse ist
|
* Prüft ob eine URL eine lokale/private Adresse ist
|
||||||
* Erlaubt:
|
* Erlaubt:
|
||||||
@@ -17,6 +27,7 @@ object UrlValidator {
|
|||||||
* - 127.x.x.x (Localhost)
|
* - 127.x.x.x (Localhost)
|
||||||
* - .local domains (mDNS/Bonjour)
|
* - .local domains (mDNS/Bonjour)
|
||||||
*/
|
*/
|
||||||
|
@Suppress("ReturnCount") // Early returns for validation checks are clearer
|
||||||
fun isLocalUrl(url: String): Boolean {
|
fun isLocalUrl(url: String): Boolean {
|
||||||
return try {
|
return try {
|
||||||
val parsedUrl = URL(url)
|
val parsedUrl = URL(url)
|
||||||
@@ -40,25 +51,29 @@ object UrlValidator {
|
|||||||
val octets = match.groupValues.drop(1).map { it.toInt() }
|
val octets = match.groupValues.drop(1).map { it.toInt() }
|
||||||
|
|
||||||
// Validate octets are in range 0-255
|
// Validate octets are in range 0-255
|
||||||
if (octets.any { it > 255 }) {
|
if (octets.any { it > OCTET_MAX_VALUE }) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
val (o1, o2, o3, o4) = octets
|
// Extract octets individually (destructuring with 4 elements triggers detekt warning)
|
||||||
|
val o1 = octets[0]
|
||||||
|
val o2 = octets[1]
|
||||||
|
|
||||||
// Check RFC 1918 private IP ranges
|
// Check RFC 1918 private IP ranges
|
||||||
return when {
|
return when {
|
||||||
// 10.0.0.0/8 (10.0.0.0 - 10.255.255.255)
|
// 10.0.0.0/8 (10.0.0.0 - 10.255.255.255)
|
||||||
o1 == 10 -> true
|
o1 == PRIVATE_CLASS_A_FIRST_OCTET -> true
|
||||||
|
|
||||||
// 172.16.0.0/12 (172.16.0.0 - 172.31.255.255)
|
// 172.16.0.0/12 (172.16.0.0 - 172.31.255.255)
|
||||||
o1 == 172 && o2 in 16..31 -> true
|
o1 == PRIVATE_CLASS_B_FIRST_OCTET &&
|
||||||
|
o2 in PRIVATE_CLASS_B_SECOND_OCTET_MIN..PRIVATE_CLASS_B_SECOND_OCTET_MAX -> true
|
||||||
|
|
||||||
// 192.168.0.0/16 (192.168.0.0 - 192.168.255.255)
|
// 192.168.0.0/16 (192.168.0.0 - 192.168.255.255)
|
||||||
o1 == 192 && o2 == 168 -> true
|
o1 == PRIVATE_CLASS_C_FIRST_OCTET &&
|
||||||
|
o2 == PRIVATE_CLASS_C_SECOND_OCTET -> true
|
||||||
|
|
||||||
// 127.0.0.0/8 (Localhost)
|
// 127.0.0.0/8 (Localhost)
|
||||||
o1 == 127 -> true
|
o1 == LOCALHOST_FIRST_OCTET -> true
|
||||||
|
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
@@ -67,7 +82,7 @@ object UrlValidator {
|
|||||||
// Not a recognized local address
|
// Not a recognized local address
|
||||||
false
|
false
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
// Invalid URL format
|
Logger.w("UrlValidator", "Failed to parse URL: ${e.message}")
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -818,6 +818,17 @@
|
|||||||
|
|
||||||
</com.google.android.material.card.MaterialCardView>
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
|
<!-- F-Droid Privacy Notice -->
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/file_logging_privacy_notice"
|
||||||
|
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
|
||||||
|
android:textColor="?attr/colorOnSurfaceVariant"
|
||||||
|
android:layout_marginBottom="16dp"
|
||||||
|
android:paddingStart="4dp"
|
||||||
|
android:paddingEnd="4dp" />
|
||||||
|
|
||||||
<!-- Export Logs Button -->
|
<!-- Export Logs Button -->
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/buttonExportLogs"
|
android:id="@+id/buttonExportLogs"
|
||||||
|
|||||||
@@ -63,4 +63,7 @@
|
|||||||
<string name="sync_status_completed">Synchronisierung abgeschlossen</string>
|
<string name="sync_status_completed">Synchronisierung abgeschlossen</string>
|
||||||
<string name="sync_status_error">Synchronisierung fehlgeschlagen</string>
|
<string name="sync_status_error">Synchronisierung fehlgeschlagen</string>
|
||||||
<string name="sync_already_running">Synchronisierung läuft bereits</string>
|
<string name="sync_already_running">Synchronisierung läuft bereits</string>
|
||||||
|
|
||||||
|
<!-- Debug/Logging Section (v1.3.2) -->
|
||||||
|
<string name="file_logging_privacy_notice">ℹ️ Datenschutz: Logs werden nur lokal auf deinem Gerät gespeichert und niemals an externe Server gesendet. Die Logs enthalten Sync-Aktivitäten zur Fehlerdiagnose. Du kannst sie jederzeit löschen oder exportieren.</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -130,6 +130,11 @@
|
|||||||
- ✅ **Password storage** - Android SharedPreferences (encrypted)
|
- ✅ **Password storage** - Android SharedPreferences (encrypted)
|
||||||
- ✅ **No third-party libs** - Only Android SDK + Sardine (WebDAV)
|
- ✅ **No third-party libs** - Only Android SDK + Sardine (WebDAV)
|
||||||
|
|
||||||
|
### Developer Features
|
||||||
|
- ✅ **File logging** - Optional, only when enabled _(NEW in v1.3.2)_
|
||||||
|
- ✅ **Privacy notice** - Explicit warning on activation
|
||||||
|
- ✅ **Local logs** - Logs stay on device
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🔋 Performance & Optimization
|
## 🔋 Performance & Optimization
|
||||||
@@ -171,6 +176,7 @@
|
|||||||
- ✅ **Dispatchers.IO** - Background operations
|
- ✅ **Dispatchers.IO** - Background operations
|
||||||
- ✅ **SharedPreferences** - Settings storage
|
- ✅ **SharedPreferences** - Settings storage
|
||||||
- ✅ **File-based storage** - JSON files locally
|
- ✅ **File-based storage** - JSON files locally
|
||||||
|
- ✅ **Custom exceptions** - Dedicated SyncException for better error handling _(NEW in v1.3.2)_
|
||||||
|
|
||||||
### Dependencies
|
### Dependencies
|
||||||
- ✅ **AndroidX** - Jetpack libraries
|
- ✅ **AndroidX** - Jetpack libraries
|
||||||
@@ -208,27 +214,22 @@
|
|||||||
|
|
||||||
## 🔮 Future Features
|
## 🔮 Future Features
|
||||||
|
|
||||||
Planned for upcoming versions (see [TODO.md](project-docs/simple-notes-sync/planning/TODO.md)):
|
Planned for upcoming versions:
|
||||||
|
|
||||||
### v1.3.0 - Web Editor & Organization
|
### v1.4.0 - Checklists
|
||||||
- ⏳ **Browser-based editor** - Edit notes in web browser
|
- ⏳ **Checklist notes** - New note type with checkboxes
|
||||||
- ⏳ **WebDAV access via browser** - No mount needed
|
- ⏳ **Completed items** - Strike-through/check off
|
||||||
- ⏳ **Mobile-optimized** - Responsive design
|
- ⏳ **Drag & drop** - Reorder items
|
||||||
- ⏳ **Offline-capable** - Progressive Web App (PWA)
|
|
||||||
- ⏳ **Tags/labels** - Categorize notes
|
|
||||||
- ⏳ **Search** - Full-text search in all notes
|
|
||||||
- ⏳ **Sorting** - By date, title, tags
|
|
||||||
- ⏳ **Filter** - Filter by tags
|
|
||||||
|
|
||||||
### v1.4.0 - Sharing & Export
|
### v1.5.0 - Internationalization
|
||||||
- ⏳ **Share note** - Via share intent
|
- ⏳ **Multi-language** - German + English UI
|
||||||
- ⏳ **Export single note** - As .txt or .md
|
- ⏳ **Language selection** - Selectable in settings
|
||||||
- ⏳ **Import from text** - Via share intent
|
- ⏳ **Full translation** - All strings in both languages
|
||||||
|
|
||||||
### v1.5.0 - Advanced Editor Features
|
### v1.6.0 - Modern APIs
|
||||||
- ⏳ **Markdown preview** - In-app rendering
|
- ⏳ **Replace LocalBroadcastManager** - Use SharedFlow instead
|
||||||
- ⏳ **Checklists** - TODO lists in notes
|
- ⏳ **PackageInfo Flags** - Use PackageInfoFlags.of()
|
||||||
- ⏳ **Syntax highlighting** - For code snippets
|
- ⏳ **Complexity refactoring** - Split long functions
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -271,4 +272,4 @@ A: Yes! Download the APK directly from GitHub or use F-Droid.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Last update:** v1.2.1 (2026-01-05)
|
**Last update:** v1.3.2 (2026-01-10)
|
||||||
|
|||||||
@@ -130,6 +130,11 @@
|
|||||||
- ✅ **Passwort-Speicherung** - Android SharedPreferences (verschlüsselt)
|
- ✅ **Passwort-Speicherung** - Android SharedPreferences (verschlüsselt)
|
||||||
- ✅ **Keine Drittanbieter-Libs** - Nur Android SDK + Sardine (WebDAV)
|
- ✅ **Keine Drittanbieter-Libs** - Nur Android SDK + Sardine (WebDAV)
|
||||||
|
|
||||||
|
### Entwickler-Features
|
||||||
|
- ✅ **Datei-Logging** - Optional, nur bei Aktivierung _(NEU in v1.3.2)_
|
||||||
|
- ✅ **Datenschutz-Hinweis** - Explizite Warnung bei Aktivierung
|
||||||
|
- ✅ **Lokale Logs** - Logs bleiben auf dem Gerät
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🔋 Performance & Optimierung
|
## 🔋 Performance & Optimierung
|
||||||
@@ -171,6 +176,7 @@
|
|||||||
- ✅ **Dispatchers.IO** - Background-Operationen
|
- ✅ **Dispatchers.IO** - Background-Operationen
|
||||||
- ✅ **SharedPreferences** - Settings-Speicherung
|
- ✅ **SharedPreferences** - Settings-Speicherung
|
||||||
- ✅ **File-Based Storage** - JSON-Dateien lokal
|
- ✅ **File-Based Storage** - JSON-Dateien lokal
|
||||||
|
- ✅ **Custom Exceptions** - Dedizierte SyncException für bessere Fehlerbehandlung _(NEU in v1.3.2)_
|
||||||
|
|
||||||
### Abhängigkeiten
|
### Abhängigkeiten
|
||||||
- ✅ **AndroidX** - Jetpack Libraries
|
- ✅ **AndroidX** - Jetpack Libraries
|
||||||
@@ -208,27 +214,22 @@
|
|||||||
|
|
||||||
## 🔮 Zukünftige Features
|
## 🔮 Zukünftige Features
|
||||||
|
|
||||||
Geplant für kommende Versionen (siehe [TODO.md](project-docs/simple-notes-sync/planning/TODO.md)):
|
Geplant für kommende Versionen:
|
||||||
|
|
||||||
### v1.3.0 - Web Editor & Organisation
|
### v1.4.0 - Checklisten
|
||||||
- ⏳ **Browser-basierter Editor** - Notizen im Webbrowser bearbeiten
|
- ⏳ **Checklisten-Notizen** - Neuer Notiz-Typ mit Checkboxen
|
||||||
- ⏳ **WebDAV-Zugriff via Browser** - Kein Mount nötig
|
- ⏳ **Erledigte Items** - Durchstreichen/Abhaken
|
||||||
- ⏳ **Mobile-optimiert** - Responsive Design
|
- ⏳ **Drag & Drop** - Items neu anordnen
|
||||||
- ⏳ **Offline-fähig** - Progressive Web App (PWA)
|
|
||||||
- ⏳ **Tags/Labels** - Kategorisierung von Notizen
|
|
||||||
- ⏳ **Suche** - Volltextsuche in allen Notizen
|
|
||||||
- ⏳ **Sortierung** - Nach Datum, Titel, Tags
|
|
||||||
- ⏳ **Filter** - Nach Tags filtern
|
|
||||||
|
|
||||||
### v1.4.0 - Sharing & Export
|
### v1.5.0 - Internationalisierung
|
||||||
- ⏳ **Notiz teilen** - Via Share-Intent
|
- ⏳ **Mehrsprachigkeit** - Deutsch + Englisch UI
|
||||||
- ⏳ **Einzelne Notiz exportieren** - Als .txt oder .md
|
- ⏳ **Sprachauswahl** - In Einstellungen wählbar
|
||||||
- ⏳ **Import von Text** - Via Share-Intent
|
- ⏳ **Vollständige Übersetzung** - Alle Strings in beiden Sprachen
|
||||||
|
|
||||||
### v1.5.0 - Erweiterte Editor-Features
|
### v1.6.0 - Modern APIs
|
||||||
- ⏳ **Markdown-Vorschau** - In-App Rendering
|
- ⏳ **LocalBroadcastManager ersetzen** - SharedFlow stattdessen
|
||||||
- ⏳ **Checklisten** - TODO-Listen in Notizen
|
- ⏳ **PackageInfo Flags** - PackageInfoFlags.of() verwenden
|
||||||
- ⏳ **Syntax-Highlighting** - Für Code-Snippets
|
- ⏳ **Komplexitäts-Refactoring** - Lange Funktionen aufteilen
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -271,4 +272,4 @@ A: Ja! Lade die APK direkt von GitHub oder nutze F-Droid.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Letzte Aktualisierung:** v1.2.1 (2026-01-05)
|
**Letzte Aktualisierung:** v1.3.2 (2026-01-10)
|
||||||
|
|||||||
5
fastlane/metadata/android/de-DE/changelogs/10.txt
Normal file
5
fastlane/metadata/android/de-DE/changelogs/10.txt
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
Unter der Haube haben wir ordentlich aufgeraumt:
|
||||||
|
- Verbesserte Sync-Performance durch optimierten Code
|
||||||
|
- Stabilere Fehlerbehandlung bei Verbindungsproblemen
|
||||||
|
- Speichereffizientere Datenverarbeitung
|
||||||
|
- Datenschutz-Hinweis fur Datei-Logging hinzugefugt
|
||||||
5
fastlane/metadata/android/en-US/changelogs/10.txt
Normal file
5
fastlane/metadata/android/en-US/changelogs/10.txt
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
Under the hood improvements:
|
||||||
|
- Improved sync performance through optimized code
|
||||||
|
- More stable error handling for connection issues
|
||||||
|
- More memory-efficient data processing
|
||||||
|
- Added privacy notice for file logging
|
||||||
Reference in New Issue
Block a user