Files
simple-notes-sync/docs/DOCS.md
inventory69 cf54f44377 docs(v1.8.1): comprehensive metadata & documentation update for F-Droid release
Complete overhaul of all metadata and documentation for v1.8.1:

F-Droid Critical:
- Fix broken umlauts in de-DE/10.txt changelog (für, hinzugefügt, aufgeräumt)
- Shorten de-DE/7.txt changelog for brevity
- Remove trailing newline from en-US/title.txt

Version & Accuracy:
- Update README badges (Android 7.0+) and footer (v1.8.1)
- Add widgets, sorting, parallel sync to highlights
- Fix APK names in workflows (removed "universal")
- Update cache action v3 → v4 in PR workflow
- Fix CONTRIBUTING.md filename references
- Update QUICKSTART guides (APK name, typo, Android version, docs links)

Feature Documentation:
- Update full_description.txt (both locales) - remove NEW/NEU labels, add v1.8.0+ features
- Major FEATURES.md update - add Views & Layout, Widgets sections, updated tech stack
- Add UPCOMING.md v1.7.0-1.8.1 as released sections
- Update DOCS.md roadmap references and dates to Feb 2026
- Create missing en-US changelogs 1.txt, 2.txt

Supplementary Fixes:
- Update BACKUP.md - add encryption docs, fix cross-references
- Add CHANGELOG.md reference links v1.2.1-v1.8.1 (15 missing)
- Fix DEBUG_APK.md duplicate header
- Rewrite fastlane/README.md with both locales and limits table
- Create SELF_SIGNED_SSL.de.md (full German translation)

Affects: 26 files across READMEs, docs/, fastlane/, workflows
2026-02-11 15:45:51 +01:00

558 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Simple Notes Sync - Technical Documentation
This file contains detailed technical information about implementation, architecture, and advanced features.
**🌍 Languages:** [Deutsch](DOCS.de.md) · **English**
---
## 📐 Architecture
### Overall Overview
```
┌─────────────────┐
│ Android App │
│ (Kotlin) │
└────────┬────────┘
│ WebDAV/HTTP
┌────────▼────────┐
│ WebDAV Server │
│ (Docker) │
└─────────────────┘
```
### Android App Architecture
```
app/
├── models/
│ ├── Note.kt # Data class for notes
│ └── SyncStatus.kt # Sync status enum
├── storage/
│ └── NotesStorage.kt # Local JSON file storage
├── sync/
│ ├── WebDavSyncService.kt # WebDAV sync logic
│ ├── NetworkMonitor.kt # WiFi detection
│ ├── SyncWorker.kt # WorkManager background worker
│ └── BootReceiver.kt # Device reboot handler
├── adapters/
│ └── NotesAdapter.kt # RecyclerView adapter
├── utils/
│ ├── Constants.kt # App constants
│ ├── NotificationHelper.kt# Notification management
│ └── Logger.kt # Debug/release logging
└── activities/
├── MainActivity.kt # Main view with list
├── NoteEditorActivity.kt# Note editor
└── SettingsActivity.kt # Server configuration
```
---
## 🔄 Auto-Sync Implementation
### WorkManager Periodic Task
Auto-sync is based on **WorkManager** with the following configuration:
```kotlin
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.UNMETERED) // WiFi only
.build()
val syncRequest = PeriodicWorkRequestBuilder<SyncWorker>(
30, TimeUnit.MINUTES, // Every 30 minutes
10, TimeUnit.MINUTES // Flex interval
)
.setConstraints(constraints)
.build()
```
**Why WorkManager?**
- ✅ Runs even when app is closed
- ✅ Automatic restart after device reboot
- ✅ Battery-efficient (Android managed)
- ✅ Guaranteed execution when constraints are met
### Network Detection
We use **Gateway IP Comparison** to check if the server is reachable:
```kotlin
fun isInHomeNetwork(): Boolean {
val gatewayIP = getGatewayIP() // e.g. 192.168.0.1
val serverIP = extractIPFromUrl(serverUrl) // e.g. 192.168.0.188
return isSameNetwork(gatewayIP, serverIP) // Checks /24 network
}
```
**Advantages:**
- ✅ No location permissions needed
- ✅ Works with all Android versions
- ✅ Reliable and fast
### Sync Flow
```
1. WorkManager wakes up (every 30 min)
2. Check: WiFi connected?
3. Check: Same network as server?
4. Load local notes
5. Upload new/changed notes → Server
6. Download remote notes ← Server
7. Merge & resolve conflicts
8. Update local storage
9. Show notification (if changes)
```
---
## 🔄 Sync Trigger Overview
The app uses **4 different sync triggers** with different use cases:
| Trigger | File | Function | When? | Pre-Check? |
|---------|------|----------|-------|------------|
| **1. Manual Sync** | `MainActivity.kt` | `triggerManualSync()` | User clicks sync button in menu | ✅ Yes |
| **2. Auto-Sync (onResume)** | `MainActivity.kt` | `triggerAutoSync()` | App opened/resumed | ✅ Yes |
| **3. Background Sync (Periodic)** | `SyncWorker.kt` | `doWork()` | Every 15/30/60 minutes (configurable) | ✅ Yes |
| **4. WiFi-Connect Sync** | `NetworkMonitor.kt``SyncWorker.kt` | `triggerWifiConnectSync()` | WiFi connected | ✅ Yes |
### Server Reachability Check (Pre-Check)
**All 4 sync triggers** use a **pre-check** before the actual sync:
```kotlin
// WebDavSyncService.kt - isServerReachable()
suspend fun isServerReachable(): Boolean = withContext(Dispatchers.IO) {
return@withContext try {
Socket().use { socket ->
socket.connect(InetSocketAddress(host, port), 2000) // 2s Timeout
}
true
} catch (e: Exception) {
Logger.d(TAG, "Server not reachable: ${e.message}")
false
}
}
```
**Why Socket Check instead of HTTP Request?**
-**Faster:** Socket connect is instant, HTTP request takes longer
- 🔋 **Battery Efficient:** No HTTP overhead (headers, TLS handshake, etc.)
- 🎯 **More Precise:** Only checks network reachability, not server logic
- 🛡️ **Prevents Errors:** Detects foreign WiFi networks before sync error occurs
**When does the check fail?**
- ❌ Server offline/unreachable
- ❌ Wrong WiFi network (e.g. public café WiFi)
- ❌ Network not ready yet (DHCP/routing delay after WiFi connect)
- ❌ VPN blocks server access
- ❌ No WebDAV server URL configured
### Sync Behavior by Trigger Type
| Trigger | When server not reachable | On successful sync | Throttling |
|---------|--------------------------|-------------------|------------|
| Manual Sync | Toast: "Server not reachable" | Toast: "✅ Synced: X notes" | None |
| Auto-Sync (onResume) | Silent abort (no toast) | Toast: "✅ Synced: X notes" | Max. 1x/min |
| Background Sync | Silent abort (no toast) | Silent (LocalBroadcast only) | 15/30/60 min |
| WiFi-Connect Sync | Silent abort (no toast) | Silent (LocalBroadcast only) | WiFi-based |
---
## 🔋 Battery Optimization
### v1.6.0: Configurable Sync Triggers
Since v1.6.0, each sync trigger can be individually enabled/disabled. This gives users fine-grained control over battery usage.
#### Sync Trigger Overview
| Trigger | Default | Battery Impact | Description |
|---------|---------|----------------|-------------|
| **Manual Sync** | Always on | 0 (user-triggered) | Toolbar button / Pull-to-refresh |
| **onSave Sync** | ✅ ON | ~0.5 mAh/save | Sync immediately after saving a note |
| **onResume Sync** | ✅ ON | ~0.3 mAh/resume | Sync when app is opened (60s throttle) |
| **WiFi-Connect** | ✅ ON | ~0.5 mAh/connect | Sync when WiFi is connected |
| **Periodic Sync** | ❌ OFF | 0.2-0.8%/day | Background sync every 15/30/60 min |
| **Boot Sync** | ❌ OFF | ~0.1 mAh/boot | Start background sync after reboot |
#### Battery Usage Calculation
**Typical usage scenario (defaults):**
- onSave: ~5 saves/day × 0.5 mAh = **~2.5 mAh**
- onResume: ~10 opens/day × 0.3 mAh = **~3 mAh**
- WiFi-Connect: ~2 connects/day × 0.5 mAh = **~1 mAh**
- **Total: ~6.5 mAh/day (~0.2% on 3000mAh battery)**
**With Periodic Sync enabled (15/30/60 min):**
| Interval | Syncs/day | Battery/day | Total (with defaults) |
|----------|-----------|-------------|----------------------|
| **15 min** | ~96 | ~23 mAh | ~30 mAh (~1.0%) |
| **30 min** | ~48 | ~12 mAh | ~19 mAh (~0.6%) |
| **60 min** | ~24 | ~6 mAh | ~13 mAh (~0.4%) |
#### Component Breakdown
| Component | Frequency | Usage | Details |
|-----------|-----------|-------|---------|
| WorkManager Wakeup | Per sync | ~0.15 mAh | System wakes up |
| Network Check | Per sync | ~0.03 mAh | Gateway IP check |
| WebDAV Sync | Only if changes | ~0.25 mAh | HTTP PUT/GET |
| **Per-Sync Total** | - | **~0.25 mAh** | Optimized |
### Optimizations
1. **Pre-Checks before Sync**
```kotlin
// Order matters! Cheapest checks first
if (!hasUnsyncedChanges()) return // Local check (cheap)
if (!isServerReachable()) return // Network check (expensive)
performSync() // Only if both pass
```
2. **Throttling**
- onResume: 60 second minimum interval
- onSave: 5 second minimum interval
- Periodic: 15/30/60 minute intervals
3. **IP Caching**
```kotlin
private var cachedServerIP: String? = null
// DNS lookup only once at start, not every check
```
4. **Conditional Logging**
```kotlin
object Logger {
fun d(tag: String, msg: String) {
if (BuildConfig.DEBUG) Log.d(tag, msg)
}
}
```
5. **Network Constraints**
- WiFi only (not mobile data)
- Only when server is reachable
- No permanent listeners
---
## 📦 WebDAV Sync Details
### Upload Flow
```kotlin
suspend fun uploadNotes(): Int {
val localNotes = storage.loadAllNotes()
var uploadedCount = 0
for (note in localNotes) {
if (note.syncStatus == SyncStatus.PENDING) {
val jsonContent = note.toJson()
val remotePath = "$serverUrl/${note.id}.json"
sardine.put(remotePath, jsonContent.toByteArray())
note.syncStatus = SyncStatus.SYNCED
storage.saveNote(note)
uploadedCount++
}
}
return uploadedCount
}
```
### Download Flow
```kotlin
suspend fun downloadNotes(): DownloadResult {
val remoteFiles = sardine.list(serverUrl)
var downloadedCount = 0
var conflictCount = 0
for (file in remoteFiles) {
if (!file.name.endsWith(".json")) continue
val content = sardine.get(file.href)
val remoteNote = Note.fromJson(content)
val localNote = storage.loadNote(remoteNote.id)
if (localNote == null) {
// New note from server
storage.saveNote(remoteNote)
downloadedCount++
} else if (localNote.modifiedAt < remoteNote.modifiedAt) {
// Server has newer version
storage.saveNote(remoteNote)
downloadedCount++
} else if (localNote.modifiedAt > remoteNote.modifiedAt) {
// Local version is newer → Conflict
resolveConflict(localNote, remoteNote)
conflictCount++
}
}
return DownloadResult(downloadedCount, conflictCount)
}
```
### Conflict Resolution
Strategy: **Last-Write-Wins** with **Conflict Copy**
```kotlin
fun resolveConflict(local: Note, remote: Note) {
// Rename remote note (conflict copy)
val conflictNote = remote.copy(
id = "${remote.id}_conflict_${System.currentTimeMillis()}",
title = "${remote.title} (Conflict)"
)
storage.saveNote(conflictNote)
// Local note remains
local.syncStatus = SyncStatus.SYNCED
storage.saveNote(local)
}
```
---
## 🔔 Notifications
### Notification Channels
```kotlin
val channel = NotificationChannel(
"notes_sync_channel",
"Notes Synchronization",
NotificationManager.IMPORTANCE_DEFAULT
)
```
### Success Notification
```kotlin
fun showSyncSuccess(context: Context, count: Int) {
val intent = Intent(context, MainActivity::class.java)
val pendingIntent = PendingIntent.getActivity(context, 0, intent, FLAGS)
val notification = NotificationCompat.Builder(context, CHANNEL_ID)
.setContentTitle("Sync successful")
.setContentText("$count notes synchronized")
.setContentIntent(pendingIntent) // Click opens app
.setAutoCancel(true) // Dismiss on click
.build()
notificationManager.notify(NOTIFICATION_ID, notification)
}
```
---
## 🛡️ Permissions
The app requires **minimal permissions**:
```xml
<!-- Network -->
<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" />
<!-- Notifications -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<!-- Boot Receiver -->
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<!-- Battery Optimization (optional) -->
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
```
**No Location Permissions!**
We use Gateway IP Comparison instead of SSID detection. No location permission required.
---
## 🧪 Testing
### Test Server
```bash
# WebDAV server reachable?
curl -u noteuser:password http://192.168.0.188:8080/
# Upload file
echo '{"test":"data"}' > test.json
curl -u noteuser:password -T test.json http://192.168.0.188:8080/test.json
# Download file
curl -u noteuser:password http://192.168.0.188:8080/test.json
```
### Test Android App
**Unit Tests:**
```bash
cd android
./gradlew test
```
**Instrumented Tests:**
```bash
./gradlew connectedAndroidTest
```
**Manual Testing Checklist:**
- [ ] Create note → visible in list
- [ ] Edit note → changes saved
- [ ] Delete note → removed from list
- [ ] Manual sync → server status "Reachable"
- [ ] Auto-sync → notification after ~30 min
- [ ] Close app → auto-sync continues
- [ ] Device reboot → auto-sync starts automatically
- [ ] Server offline → error notification
- [ ] Notification click → app opens
---
## 🚀 Build & Deployment
### Debug Build
```bash
cd android
./gradlew assembleDebug
# APK: app/build/outputs/apk/debug/app-debug.apk
```
### Release Build
```bash
./gradlew assembleRelease
# APK: app/build/outputs/apk/release/app-release-unsigned.apk
```
### Sign (for Distribution)
```bash
# Create keystore
keytool -genkey -v -keystore my-release-key.jks -keyalg RSA -keysize 2048 -validity 10000 -alias my-alias
# Sign APK
jarsigner -verbose -sigalg SHA256withRSA -digestalg SHA-256 \
-keystore my-release-key.jks \
app-release-unsigned.apk my-alias
# Optimize
zipalign -v 4 app-release-unsigned.apk app-release.apk
```
---
## 🐛 Debugging
### LogCat Filter
```bash
# Only app logs
adb logcat -s SimpleNotesApp NetworkMonitor SyncWorker WebDavSyncService
# With timestamps
adb logcat -v time -s SyncWorker
# Save to file
adb logcat -s SyncWorker > sync_debug.log
```
### Common Issues
**Problem: Auto-sync not working**
```
Solution: Disable battery optimization
Settings → Apps → Simple Notes → Battery → Don't optimize
```
**Problem: Server not reachable**
```
Check:
1. Server running? → docker-compose ps
2. IP correct? → ip addr show
3. Port open? → telnet 192.168.0.188 8080
4. Firewall? → sudo ufw allow 8080
```
**Problem: Notifications not appearing**
```
Check:
1. Notification permission granted?
2. Do Not Disturb active?
3. App in background? → Force stop & restart
```
---
## 📚 Dependencies
```gradle
// Core
androidx.core:core-ktx:1.12.0
androidx.appcompat:appcompat:1.6.1
com.google.android.material:material:1.11.0
// Lifecycle
androidx.lifecycle:lifecycle-runtime-ktx:2.7.0
// RecyclerView
androidx.recyclerview:recyclerview:1.3.2
// Coroutines
org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3
// WorkManager
androidx.work:work-runtime-ktx:2.9.0
// WebDAV Client
com.github.thegrizzlylabs:sardine-android:0.8
// Broadcast (deprecated but working)
androidx.localbroadcastmanager:localbroadcastmanager:1.1.0
```
---
## 🔮 Roadmap
See [UPCOMING.md](UPCOMING.md) for the full roadmap and planned features.
---
## 📖 Further Documentation
- [Project Docs](https://github.com/inventory69/project-docs/tree/main/simple-notes-sync)
- [Sync Architecture](https://github.com/inventory69/project-docs/blob/main/simple-notes-sync/SYNC_ARCHITECTURE.md) - **Detailed Sync Trigger Documentation**
- [Android Guide](https://github.com/inventory69/project-docs/blob/main/simple-notes-sync/ANDROID_GUIDE.md)
- [Bugfix Documentation](https://github.com/inventory69/project-docs/blob/main/simple-notes-sync/BUGFIX_SYNC_SPAM_AND_NOTIFICATIONS.md)
---
**Last updated:** February 2026