🚀 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:
198
.github/workflows/build-production-apk.yml
vendored
Normal file
198
.github/workflows/build-production-apk.yml
vendored
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
name: Build Android Production APK
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main ] # Trigger on push to main branch
|
||||||
|
workflow_dispatch: # Allow manual trigger
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write # Required for creating releases
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build Production APK
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Java
|
||||||
|
uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
distribution: 'temurin'
|
||||||
|
java-version: '17'
|
||||||
|
|
||||||
|
- name: Generate Production version number
|
||||||
|
run: |
|
||||||
|
# Generate semantic version: YYYY.MM.DD
|
||||||
|
VERSION_NAME="$(date +'%Y.%m.%d')"
|
||||||
|
|
||||||
|
# Use GitHub run number as build number for production
|
||||||
|
BUILD_NUMBER="${{ github.run_number }}"
|
||||||
|
|
||||||
|
echo "VERSION_NAME=$VERSION_NAME" >> $GITHUB_ENV
|
||||||
|
echo "BUILD_NUMBER=$BUILD_NUMBER" >> $GITHUB_ENV
|
||||||
|
echo "VERSION_TAG=v$VERSION_NAME-prod.$BUILD_NUMBER" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
echo "🚀 Generated PRODUCTION version: $VERSION_NAME+$BUILD_NUMBER"
|
||||||
|
|
||||||
|
- name: Update build.gradle.kts with Production version
|
||||||
|
run: |
|
||||||
|
# Update versionCode and versionName in build.gradle.kts
|
||||||
|
sed -i "s/versionCode = [0-9]*/versionCode = ${{ env.BUILD_NUMBER }}/" android/app/build.gradle.kts
|
||||||
|
sed -i "s/versionName = \".*\"/versionName = \"${{ env.VERSION_NAME }}\"/" android/app/build.gradle.kts
|
||||||
|
|
||||||
|
echo "✅ Updated build.gradle.kts:"
|
||||||
|
grep -E "versionCode|versionName" android/app/build.gradle.kts
|
||||||
|
|
||||||
|
- name: Setup Android signing
|
||||||
|
run: |
|
||||||
|
echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 -d > android/app/simple-notes-release.jks
|
||||||
|
echo "storePassword=${{ secrets.KEYSTORE_PASSWORD }}" > android/key.properties
|
||||||
|
echo "keyPassword=${{ secrets.KEY_PASSWORD }}" >> android/key.properties
|
||||||
|
echo "keyAlias=${{ secrets.KEY_ALIAS }}" >> android/key.properties
|
||||||
|
echo "storeFile=simple-notes-release.jks" >> android/key.properties
|
||||||
|
echo "✅ Signing configuration created"
|
||||||
|
|
||||||
|
- name: Build Production APK (Release)
|
||||||
|
run: |
|
||||||
|
cd android
|
||||||
|
./gradlew assembleRelease --no-daemon --stacktrace
|
||||||
|
|
||||||
|
- name: Copy APK variants to root with version names
|
||||||
|
run: |
|
||||||
|
mkdir -p apk-output
|
||||||
|
|
||||||
|
# Universal APK
|
||||||
|
cp android/app/build/outputs/apk/release/app-universal-release.apk \
|
||||||
|
apk-output/simple-notes-sync-v${{ env.VERSION_NAME }}-universal.apk
|
||||||
|
|
||||||
|
# ARM64 APK
|
||||||
|
cp android/app/build/outputs/apk/release/app-arm64-v8a-release.apk \
|
||||||
|
apk-output/simple-notes-sync-v${{ env.VERSION_NAME }}-arm64-v8a.apk
|
||||||
|
|
||||||
|
# ARMv7 APK
|
||||||
|
cp android/app/build/outputs/apk/release/app-armeabi-v7a-release.apk \
|
||||||
|
apk-output/simple-notes-sync-v${{ env.VERSION_NAME }}-armeabi-v7a.apk
|
||||||
|
|
||||||
|
echo "✅ APK files prepared:"
|
||||||
|
ls -lh apk-output/
|
||||||
|
|
||||||
|
- name: Upload APK artifacts
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: simple-notes-sync-apks-v${{ env.VERSION_NAME }}
|
||||||
|
path: apk-output/*.apk
|
||||||
|
retention-days: 90 # Keep production builds longer
|
||||||
|
|
||||||
|
- name: Get commit info
|
||||||
|
run: |
|
||||||
|
echo "SHORT_SHA=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
|
||||||
|
echo "COMMIT_DATE=$(git log -1 --format=%cd --date=iso-strict)" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
# Get full commit message preserving newlines and emojis (UTF-8)
|
||||||
|
{
|
||||||
|
echo 'COMMIT_MSG<<EOF'
|
||||||
|
git -c core.quotepath=false log -1 --pretty=%B
|
||||||
|
echo 'EOF'
|
||||||
|
} >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Create Production Release
|
||||||
|
uses: softprops/action-gh-release@v1
|
||||||
|
with:
|
||||||
|
tag_name: ${{ env.VERSION_TAG }}
|
||||||
|
name: "📝 Simple Notes Sync v${{ env.VERSION_NAME }} (Production)"
|
||||||
|
files: apk-output/*.apk
|
||||||
|
draft: false
|
||||||
|
prerelease: false
|
||||||
|
generate_release_notes: false
|
||||||
|
body: |
|
||||||
|
# 📝 Production Release: Simple Notes Sync v${{ env.VERSION_NAME }}
|
||||||
|
|
||||||
|
## Build Information
|
||||||
|
|
||||||
|
- **Version:** ${{ env.VERSION_NAME }}+${{ env.BUILD_NUMBER }}
|
||||||
|
- **Build Date:** ${{ env.COMMIT_DATE }}
|
||||||
|
- **Commit:** ${{ env.SHORT_SHA }}
|
||||||
|
- **Environment:** 🟢 **PRODUCTION**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Changes
|
||||||
|
|
||||||
|
${{ env.COMMIT_MSG }}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📦 Download & Installation
|
||||||
|
|
||||||
|
### Which APK should I download?
|
||||||
|
|
||||||
|
| Your Device | Download This APK | Size | Compatibility |
|
||||||
|
|-------------|------------------|------|---------------|
|
||||||
|
| 🤷 Not sure? | `simple-notes-sync-v${{ env.VERSION_NAME }}-universal.apk` | ~5 MB | Works on all devices |
|
||||||
|
| Modern (2018+) | `simple-notes-sync-v${{ env.VERSION_NAME }}-arm64-v8a.apk` | ~3 MB | Faster, smaller |
|
||||||
|
| Older devices | `simple-notes-sync-v${{ env.VERSION_NAME }}-armeabi-v7a.apk` | ~3 MB | Older ARM chips |
|
||||||
|
|
||||||
|
### Installation Steps
|
||||||
|
1. Download the appropriate APK from the assets below
|
||||||
|
2. Enable "Install from unknown sources" in Android settings
|
||||||
|
3. Open the downloaded APK file
|
||||||
|
4. Follow the installation prompts
|
||||||
|
5. Configure WebDAV settings in the app
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚙️ Features
|
||||||
|
|
||||||
|
- ✅ Automatic WebDAV sync every 30 minutes (~0.4% battery/day)
|
||||||
|
- ✅ Smart gateway detection (home network auto-detection)
|
||||||
|
- ✅ Material Design 3 UI
|
||||||
|
- ✅ Privacy-focused (no tracking, no analytics)
|
||||||
|
- ✅ Offline-first architecture
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 Updating from Previous Version
|
||||||
|
|
||||||
|
Simply install this APK over the existing installation - all data and settings will be preserved.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📱 Obtanium - Auto-Update App
|
||||||
|
|
||||||
|
Get automatic updates with [Obtanium](https://github.com/ImranR98/Obtanium/releases/latest).
|
||||||
|
|
||||||
|
**Setup:**
|
||||||
|
1. Install Obtanium from the link above
|
||||||
|
2. Add app with this URL: `https://github.com/dettmersLiq/simple-notes-sync`
|
||||||
|
3. Enable auto-updates
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🆘 Support
|
||||||
|
|
||||||
|
For issues or questions, please open an issue on GitHub.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔒 Privacy & Security
|
||||||
|
|
||||||
|
- All data synced via your own WebDAV server
|
||||||
|
- No third-party analytics or tracking
|
||||||
|
- No internet permissions except for WebDAV sync
|
||||||
|
- All sync operations encrypted (HTTPS)
|
||||||
|
- Open source - audit the code yourself
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ Built With
|
||||||
|
|
||||||
|
- **Language:** Kotlin
|
||||||
|
- **UI:** Material Design 3
|
||||||
|
- **Sync:** WorkManager + WebDAV
|
||||||
|
- **Target SDK:** Android 16 (API 36)
|
||||||
|
- **Min SDK:** Android 8.0 (API 26)
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -15,6 +15,11 @@ android/*.apk
|
|||||||
android/*.ap_
|
android/*.ap_
|
||||||
android/*.aab
|
android/*.aab
|
||||||
|
|
||||||
|
# Signing files (NEVER commit these!)
|
||||||
|
android/key.properties
|
||||||
|
android/app/*.jks
|
||||||
|
android/app/*.keystore
|
||||||
|
|
||||||
# Gradle
|
# Gradle
|
||||||
.gradle
|
.gradle
|
||||||
build/
|
build/
|
||||||
|
|||||||
472
DOCS.md
Normal file
472
DOCS.md
Normal file
@@ -0,0 +1,472 @@
|
|||||||
|
# Simple Notes Sync - Technische Dokumentation
|
||||||
|
|
||||||
|
Diese Datei enthält detaillierte technische Informationen über die Implementierung, Architektur und erweiterte Funktionen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📐 Architektur
|
||||||
|
|
||||||
|
### Gesamtübersicht
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────┐
|
||||||
|
│ Android App │
|
||||||
|
│ (Kotlin) │
|
||||||
|
└────────┬────────┘
|
||||||
|
│ WebDAV/HTTP
|
||||||
|
│
|
||||||
|
┌────────▼────────┐
|
||||||
|
│ WebDAV Server │
|
||||||
|
│ (Docker) │
|
||||||
|
└─────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Android App Architektur
|
||||||
|
|
||||||
|
```
|
||||||
|
app/
|
||||||
|
├── models/
|
||||||
|
│ ├── Note.kt # Data class für Notizen
|
||||||
|
│ └── SyncStatus.kt # Sync-Status Enum
|
||||||
|
├── storage/
|
||||||
|
│ └── NotesStorage.kt # Lokale JSON-Datei Speicherung
|
||||||
|
├── sync/
|
||||||
|
│ ├── WebDavSyncService.kt # WebDAV Sync-Logik
|
||||||
|
│ ├── NetworkMonitor.kt # WLAN-Erkennung
|
||||||
|
│ ├── SyncWorker.kt # WorkManager Background Worker
|
||||||
|
│ └── BootReceiver.kt # Device Reboot Handler
|
||||||
|
├── adapters/
|
||||||
|
│ └── NotesAdapter.kt # RecyclerView Adapter
|
||||||
|
├── utils/
|
||||||
|
│ ├── Constants.kt # App-Konstanten
|
||||||
|
│ ├── NotificationHelper.kt# Notification Management
|
||||||
|
│ └── Logger.kt # Debug/Release Logging
|
||||||
|
└── activities/
|
||||||
|
├── MainActivity.kt # Hauptansicht mit Liste
|
||||||
|
├── NoteEditorActivity.kt# Editor für Notizen
|
||||||
|
└── SettingsActivity.kt # Server-Konfiguration
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 Auto-Sync Implementierung
|
||||||
|
|
||||||
|
### WorkManager Periodic Task
|
||||||
|
|
||||||
|
Der Auto-Sync basiert auf **WorkManager** mit folgender Konfiguration:
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
val constraints = Constraints.Builder()
|
||||||
|
.setRequiredNetworkType(NetworkType.UNMETERED) // Nur WiFi
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val syncRequest = PeriodicWorkRequestBuilder<SyncWorker>(
|
||||||
|
30, TimeUnit.MINUTES, // Alle 30 Minuten
|
||||||
|
10, TimeUnit.MINUTES // Flex interval
|
||||||
|
)
|
||||||
|
.setConstraints(constraints)
|
||||||
|
.build()
|
||||||
|
```
|
||||||
|
|
||||||
|
**Warum WorkManager?**
|
||||||
|
- ✅ Läuft auch wenn App geschlossen ist
|
||||||
|
- ✅ Automatischer Restart nach Device Reboot
|
||||||
|
- ✅ Battery-efficient (Android managed)
|
||||||
|
- ✅ Garantierte Ausführung bei erfüllten Constraints
|
||||||
|
|
||||||
|
### Network Detection
|
||||||
|
|
||||||
|
Statt SSID-basierter Erkennung (Android 13+ Privacy-Probleme) verwenden wir **Gateway IP Comparison**:
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
fun isInHomeNetwork(): Boolean {
|
||||||
|
val gatewayIP = getGatewayIP() // z.B. 192.168.0.1
|
||||||
|
val serverIP = extractIPFromUrl(serverUrl) // z.B. 192.168.0.188
|
||||||
|
|
||||||
|
return isSameNetwork(gatewayIP, serverIP) // Prüft /24 Netzwerk
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Vorteile:**
|
||||||
|
- ✅ Keine Location Permissions nötig
|
||||||
|
- ✅ Funktioniert mit allen Android Versionen
|
||||||
|
- ✅ Zuverlässig und schnell
|
||||||
|
|
||||||
|
### Sync Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
1. WorkManager wacht auf (alle 30 Min)
|
||||||
|
↓
|
||||||
|
2. Check: WiFi connected?
|
||||||
|
↓
|
||||||
|
3. Check: Same network as server?
|
||||||
|
↓
|
||||||
|
4. Load local notes
|
||||||
|
↓
|
||||||
|
5. Upload neue/geänderte Notes → Server
|
||||||
|
↓
|
||||||
|
6. Download remote notes ← Server
|
||||||
|
↓
|
||||||
|
7. Merge & resolve conflicts
|
||||||
|
↓
|
||||||
|
8. Update local storage
|
||||||
|
↓
|
||||||
|
9. Show notification (if changes)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔋 Akku-Optimierung
|
||||||
|
|
||||||
|
### Verbrauchsanalyse
|
||||||
|
|
||||||
|
| Komponente | Frequenz | Verbrauch | Details |
|
||||||
|
|------------|----------|-----------|---------|
|
||||||
|
| WorkManager Wakeup | Alle 30 Min | ~0.15 mAh | System wacht auf |
|
||||||
|
| Network Check | 48x/Tag | ~0.03 mAh | Gateway IP check |
|
||||||
|
| WebDAV Sync | 2-3x/Tag | ~1.5 mAh | Nur bei Änderungen |
|
||||||
|
| **Total** | - | **~12 mAh/Tag** | **~0.4%** bei 3000mAh |
|
||||||
|
|
||||||
|
### Optimierungen
|
||||||
|
|
||||||
|
1. **IP Caching**
|
||||||
|
```kotlin
|
||||||
|
private var cachedServerIP: String? = null
|
||||||
|
// DNS lookup nur 1x beim Start, nicht bei jedem Check
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Throttling**
|
||||||
|
```kotlin
|
||||||
|
private var lastSyncTime = 0L
|
||||||
|
private const val MIN_SYNC_INTERVAL_MS = 60_000L // Max 1 Sync/Min
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Conditional Logging**
|
||||||
|
```kotlin
|
||||||
|
object Logger {
|
||||||
|
fun d(tag: String, msg: String) {
|
||||||
|
if (BuildConfig.DEBUG) Log.d(tag, msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Network Constraints**
|
||||||
|
- Nur WiFi (nicht mobile Daten)
|
||||||
|
- Nur wenn Server erreichbar
|
||||||
|
- Keine permanenten 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) {
|
||||||
|
// Neue Note vom Server
|
||||||
|
storage.saveNote(remoteNote)
|
||||||
|
downloadedCount++
|
||||||
|
} else if (localNote.modifiedAt < remoteNote.modifiedAt) {
|
||||||
|
// Server hat neuere Version
|
||||||
|
storage.saveNote(remoteNote)
|
||||||
|
downloadedCount++
|
||||||
|
} else if (localNote.modifiedAt > remoteNote.modifiedAt) {
|
||||||
|
// Lokale Version ist neuer → Conflict
|
||||||
|
resolveConflict(localNote, remoteNote)
|
||||||
|
conflictCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return DownloadResult(downloadedCount, conflictCount)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Conflict Resolution
|
||||||
|
|
||||||
|
Strategie: **Last-Write-Wins** mit **Conflict Copy**
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
fun resolveConflict(local: Note, remote: Note) {
|
||||||
|
// Remote Note umbenennen (Conflict Copy)
|
||||||
|
val conflictNote = remote.copy(
|
||||||
|
id = "${remote.id}_conflict_${System.currentTimeMillis()}",
|
||||||
|
title = "${remote.title} (Konflikt)"
|
||||||
|
)
|
||||||
|
|
||||||
|
storage.saveNote(conflictNote)
|
||||||
|
|
||||||
|
// Lokale Note bleibt
|
||||||
|
local.syncStatus = SyncStatus.SYNCED
|
||||||
|
storage.saveNote(local)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔔 Notifications
|
||||||
|
|
||||||
|
### Notification Channels
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
val channel = NotificationChannel(
|
||||||
|
"notes_sync_channel",
|
||||||
|
"Notizen Synchronisierung",
|
||||||
|
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 erfolgreich")
|
||||||
|
.setContentText("$count Notizen synchronisiert")
|
||||||
|
.setContentIntent(pendingIntent) // Click öffnet App
|
||||||
|
.setAutoCancel(true) // Dismiss on click
|
||||||
|
.build()
|
||||||
|
|
||||||
|
notificationManager.notify(NOTIFICATION_ID, notification)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛡️ Permissions
|
||||||
|
|
||||||
|
Die App benötigt **minimale Permissions**:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<!-- Netzwerk -->
|
||||||
|
<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" />
|
||||||
|
```
|
||||||
|
|
||||||
|
**Keine Location Permissions!**
|
||||||
|
Frühere Versionen benötigten `ACCESS_FINE_LOCATION` für SSID-Erkennung. Jetzt verwenden wir Gateway IP Comparison.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧪 Testing
|
||||||
|
|
||||||
|
### Server testen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# WebDAV Server erreichbar?
|
||||||
|
curl -u noteuser:password http://192.168.0.188:8080/
|
||||||
|
|
||||||
|
# Datei hochladen
|
||||||
|
echo '{"test":"data"}' > test.json
|
||||||
|
curl -u noteuser:password -T test.json http://192.168.0.188:8080/test.json
|
||||||
|
|
||||||
|
# Datei herunterladen
|
||||||
|
curl -u noteuser:password http://192.168.0.188:8080/test.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### Android App testen
|
||||||
|
|
||||||
|
**Unit Tests:**
|
||||||
|
```bash
|
||||||
|
cd android
|
||||||
|
./gradlew test
|
||||||
|
```
|
||||||
|
|
||||||
|
**Instrumented Tests:**
|
||||||
|
```bash
|
||||||
|
./gradlew connectedAndroidTest
|
||||||
|
```
|
||||||
|
|
||||||
|
**Manual Testing Checklist:**
|
||||||
|
|
||||||
|
- [ ] Notiz erstellen → in Liste sichtbar
|
||||||
|
- [ ] Notiz bearbeiten → Änderungen gespeichert
|
||||||
|
- [ ] Notiz löschen → aus Liste entfernt
|
||||||
|
- [ ] Manueller Sync → Server Status "Erreichbar"
|
||||||
|
- [ ] Auto-Sync → Notification nach ~30 Min
|
||||||
|
- [ ] App schließen → Auto-Sync funktioniert weiter
|
||||||
|
- [ ] Device Reboot → Auto-Sync startet automatisch
|
||||||
|
- [ ] Server offline → Error Notification
|
||||||
|
- [ ] Notification Click → App öffnet sich
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 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
|
||||||
|
```
|
||||||
|
|
||||||
|
### Signieren (für Distribution)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Keystore erstellen
|
||||||
|
keytool -genkey -v -keystore my-release-key.jks -keyalg RSA -keysize 2048 -validity 10000 -alias my-alias
|
||||||
|
|
||||||
|
# APK signieren
|
||||||
|
jarsigner -verbose -sigalg SHA256withRSA -digestalg SHA-256 \
|
||||||
|
-keystore my-release-key.jks \
|
||||||
|
app-release-unsigned.apk my-alias
|
||||||
|
|
||||||
|
# Optimieren
|
||||||
|
zipalign -v 4 app-release-unsigned.apk app-release.apk
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🐛 Debugging
|
||||||
|
|
||||||
|
### LogCat Filter
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Nur App-Logs
|
||||||
|
adb logcat -s SimpleNotesApp NetworkMonitor SyncWorker WebDavSyncService
|
||||||
|
|
||||||
|
# Mit Timestamps
|
||||||
|
adb logcat -v time -s SyncWorker
|
||||||
|
|
||||||
|
# In Datei speichern
|
||||||
|
adb logcat -s SyncWorker > sync_debug.log
|
||||||
|
```
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
**Problem: Auto-Sync funktioniert nicht**
|
||||||
|
```
|
||||||
|
Lösung: Akku-Optimierung deaktivieren
|
||||||
|
Settings → Apps → Simple Notes → Battery → Don't optimize
|
||||||
|
```
|
||||||
|
|
||||||
|
**Problem: Server nicht erreichbar**
|
||||||
|
```
|
||||||
|
Check:
|
||||||
|
1. Server läuft? → docker-compose ps
|
||||||
|
2. IP korrekt? → ip addr show
|
||||||
|
3. Port offen? → telnet 192.168.0.188 8080
|
||||||
|
4. Firewall? → sudo ufw allow 8080
|
||||||
|
```
|
||||||
|
|
||||||
|
**Problem: Notifications kommen nicht**
|
||||||
|
```
|
||||||
|
Check:
|
||||||
|
1. Notification Permission erteilt?
|
||||||
|
2. Do Not Disturb aktiv?
|
||||||
|
3. App im 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
|
||||||
|
|
||||||
|
### v1.1
|
||||||
|
- [ ] Suche & Filter
|
||||||
|
- [ ] Dark Mode
|
||||||
|
- [ ] Tags/Kategorien
|
||||||
|
- [ ] Markdown Preview
|
||||||
|
|
||||||
|
### v2.0
|
||||||
|
- [ ] Desktop Client (Flutter)
|
||||||
|
- [ ] End-to-End Verschlüsselung
|
||||||
|
- [ ] Shared Notes (Collaboration)
|
||||||
|
- [ ] Attachment Support
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📖 Weitere Dokumentation
|
||||||
|
|
||||||
|
- [Project Docs](https://github.com/inventory69/project-docs/tree/main/simple-notes-sync)
|
||||||
|
- [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)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Letzte Aktualisierung:** 21. Dezember 2025
|
||||||
188
GITHUB_ACTIONS_SETUP.md
Normal file
188
GITHUB_ACTIONS_SETUP.md
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
# GitHub Actions Setup Guide
|
||||||
|
|
||||||
|
This guide explains how to set up the GitHub Actions workflow for automated APK builds with proper signing.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The workflow in `.github/workflows/build-production-apk.yml` automatically:
|
||||||
|
- Builds signed APKs on every push to `main`
|
||||||
|
- Generates version numbers using `YYYY.MM.DD` + build number
|
||||||
|
- Creates 3 APK variants (universal, arm64-v8a, armeabi-v7a)
|
||||||
|
- Creates GitHub releases with all APKs attached
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- GitHub CLI (`gh`) installed
|
||||||
|
- Java 17+ installed (for keytool)
|
||||||
|
- Git repository initialized with GitHub remote
|
||||||
|
|
||||||
|
## Step 1: Generate Signing Keystore
|
||||||
|
|
||||||
|
⚠️ **IMPORTANT**: Store the keystore securely! Without it, you cannot publish updates to your app.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Navigate to project root
|
||||||
|
cd /path/to/simple-notes-sync
|
||||||
|
|
||||||
|
# Generate keystore (replace values as needed)
|
||||||
|
keytool -genkey -v \
|
||||||
|
-keystore android/app/simple-notes-release.jks \
|
||||||
|
-keyalg RSA \
|
||||||
|
-keysize 2048 \
|
||||||
|
-validity 10000 \
|
||||||
|
-alias simple-notes
|
||||||
|
|
||||||
|
# You will be prompted for:
|
||||||
|
# - Keystore password (remember this!)
|
||||||
|
# - Key password (remember this!)
|
||||||
|
# - Your name, organization, etc.
|
||||||
|
```
|
||||||
|
|
||||||
|
**Store these securely:**
|
||||||
|
- Keystore password
|
||||||
|
- Key password
|
||||||
|
- Alias: `simple-notes`
|
||||||
|
- Keystore file: `android/app/simple-notes-release.jks`
|
||||||
|
|
||||||
|
⚠️ **BACKUP**: Make a backup of the keystore file in a secure location (NOT in the repository).
|
||||||
|
|
||||||
|
## Step 2: Base64 Encode Keystore
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create base64 encoded version
|
||||||
|
base64 android/app/simple-notes-release.jks > simple-notes-release.jks.b64
|
||||||
|
|
||||||
|
# Or on macOS:
|
||||||
|
base64 -i android/app/simple-notes-release.jks -o simple-notes-release.jks.b64
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 3: Set GitHub Secrets
|
||||||
|
|
||||||
|
Using GitHub CLI (recommended):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Set KEYSTORE_BASE64 secret
|
||||||
|
gh secret set KEYSTORE_BASE64 < simple-notes-release.jks.b64
|
||||||
|
|
||||||
|
# Set KEYSTORE_PASSWORD (will prompt for input)
|
||||||
|
gh secret set KEYSTORE_PASSWORD
|
||||||
|
|
||||||
|
# Set KEY_PASSWORD (will prompt for input)
|
||||||
|
gh secret set KEY_PASSWORD
|
||||||
|
|
||||||
|
# Set KEY_ALIAS (value: simple-notes)
|
||||||
|
printf "simple-notes" | gh secret set KEY_ALIAS
|
||||||
|
```
|
||||||
|
|
||||||
|
Or manually via GitHub web interface:
|
||||||
|
1. Go to repository Settings → Secrets and variables → Actions
|
||||||
|
2. Click "New repository secret"
|
||||||
|
3. Add these secrets:
|
||||||
|
- `KEYSTORE_BASE64`: Paste content of `simple-notes-release.jks.b64`
|
||||||
|
- `KEYSTORE_PASSWORD`: Your keystore password
|
||||||
|
- `KEY_PASSWORD`: Your key password
|
||||||
|
- `KEY_ALIAS`: `simple-notes`
|
||||||
|
|
||||||
|
## Step 4: Verify Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check secrets are set
|
||||||
|
gh secret list
|
||||||
|
|
||||||
|
# Expected output:
|
||||||
|
# KEYSTORE_BASE64 Updated YYYY-MM-DD
|
||||||
|
# KEYSTORE_PASSWORD Updated YYYY-MM-DD
|
||||||
|
# KEY_PASSWORD Updated YYYY-MM-DD
|
||||||
|
# KEY_ALIAS Updated YYYY-MM-DD
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 5: Cleanup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Remove sensitive files (they're in .gitignore, but double-check)
|
||||||
|
rm simple-notes-release.jks.b64
|
||||||
|
rm -f android/key.properties # Generated by workflow
|
||||||
|
|
||||||
|
# Verify keystore is NOT tracked by git
|
||||||
|
git status | grep -i jks
|
||||||
|
# Should return nothing
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 6: Trigger First Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Commit and push to main
|
||||||
|
git add .
|
||||||
|
git commit -m "🚀 feat: Add GitHub Actions deployment workflow"
|
||||||
|
git push origin main
|
||||||
|
|
||||||
|
# Or manually trigger workflow
|
||||||
|
gh workflow run build-production-apk.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
1. Go to GitHub repository → Actions tab
|
||||||
|
2. Check workflow run status
|
||||||
|
3. Once complete, go to Releases tab
|
||||||
|
4. Verify release was created with 3 APK variants
|
||||||
|
5. Download and test one of the APKs
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Build fails with "Keystore not found"
|
||||||
|
- Check `KEYSTORE_BASE64` secret is set correctly
|
||||||
|
- Verify base64 encoding was done without line breaks
|
||||||
|
|
||||||
|
### Build fails with "Incorrect password"
|
||||||
|
- Verify `KEYSTORE_PASSWORD` and `KEY_PASSWORD` are correct
|
||||||
|
- Re-set secrets if needed
|
||||||
|
|
||||||
|
### APK files not found
|
||||||
|
- Check build logs for errors in assembleRelease step
|
||||||
|
- Verify APK output paths match workflow expectations
|
||||||
|
|
||||||
|
### Updates don't work
|
||||||
|
- Ensure you're using the same keystore for all builds
|
||||||
|
- Verify `applicationId` in build.gradle.kts matches
|
||||||
|
|
||||||
|
## Security Notes
|
||||||
|
|
||||||
|
- ✅ Keystore is base64-encoded in GitHub secrets (secure)
|
||||||
|
- ✅ Passwords are stored in GitHub secrets (encrypted)
|
||||||
|
- ✅ `key.properties` and `.jks` files are in `.gitignore`
|
||||||
|
- ⚠️ Never commit keystore files to repository
|
||||||
|
- ⚠️ Keep backup of keystore in secure location
|
||||||
|
- ⚠️ Don't share keystore passwords
|
||||||
|
|
||||||
|
## Versioning
|
||||||
|
|
||||||
|
Versions follow this pattern:
|
||||||
|
- **Version Name**: `YYYY.MM.DD` (e.g., `2025.01.15`)
|
||||||
|
- **Version Code**: GitHub run number (e.g., `42`)
|
||||||
|
- **Release Tag**: `vYYYY.MM.DD-prod.BUILD` (e.g., `v2025.01.15-prod.42`)
|
||||||
|
|
||||||
|
This ensures:
|
||||||
|
- Semantic versioning based on release date
|
||||||
|
- Incremental version codes for Play Store compatibility
|
||||||
|
- Clear distinction between builds
|
||||||
|
|
||||||
|
## APK Variants
|
||||||
|
|
||||||
|
The workflow generates 3 APK variants:
|
||||||
|
|
||||||
|
1. **Universal APK** (~5 MB)
|
||||||
|
- Works on all devices
|
||||||
|
- Larger file size
|
||||||
|
- Recommended for most users
|
||||||
|
|
||||||
|
2. **arm64-v8a APK** (~3 MB)
|
||||||
|
- For modern devices (2018+)
|
||||||
|
- Smaller, faster
|
||||||
|
- 64-bit ARM processors
|
||||||
|
|
||||||
|
3. **armeabi-v7a APK** (~3 MB)
|
||||||
|
- For older devices
|
||||||
|
- 32-bit ARM processors
|
||||||
|
|
||||||
|
Users can choose based on their device - Obtanium auto-updates work with all variants.
|
||||||
350
README.md
350
README.md
@@ -1,253 +1,153 @@
|
|||||||
# Simple Notes Sync
|
# Simple Notes Sync 📝
|
||||||
|
|
||||||
Minimalistische Offline-Notiz-App mit automatischer WLAN-Synchronisierung.
|
> Minimalistische Android-App für Offline-Notizen mit automatischer WLAN-Synchronisierung
|
||||||
|
|
||||||
## 📱 Features
|
Eine schlanke Notiz-App ohne Schnickschnack - perfekt für schnelle Gedanken, die automatisch zu Hause synchronisiert werden.
|
||||||
|
|
||||||
- ✅ Offline-first: Notizen lokal erstellen und bearbeiten
|
---
|
||||||
- ✅ Auto-Sync: Automatische Synchronisierung im Heim-WLAN
|
|
||||||
- ✅ WebDAV: Docker-basierter Server
|
|
||||||
- ✅ Simpel: Fokus auf Funktionalität
|
|
||||||
- ✅ Robust: Fehlerbehandlung und Konfliktauflösung
|
|
||||||
|
|
||||||
## 🏗️ Projekt-Struktur
|
## ✨ Features
|
||||||
|
|
||||||
```
|
- 📝 **Offline-first** - Notizen werden lokal gespeichert und sind immer verfügbar
|
||||||
simple-notes-sync/
|
- 🔄 **Auto-Sync** - Automatische Synchronisierung wenn du im Heimnetzwerk bist
|
||||||
├── server/ # Docker WebDAV Server
|
- 🏠 **WebDAV Server** - Deine Daten bleiben bei dir (Docker-Container)
|
||||||
│ ├── docker-compose.yml
|
- 🔋 **Akkuschonend** - Nur ~0.4% Akkuverbrauch pro Tag
|
||||||
│ ├── .env.example
|
- 🚫 **Keine Cloud** - Keine Google, keine Microsoft, keine Drittanbieter
|
||||||
│ └── README.md
|
- 🔐 **Privacy** - Keine Tracking, keine Analytics, keine Standort-Berechtigungen
|
||||||
│
|
|
||||||
└── android/ # Android App (Kotlin)
|
|
||||||
└── (Android Studio Projekt)
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🚀 Quick Start
|
---
|
||||||
|
|
||||||
### 1. Server starten
|
## 📥 Installation
|
||||||
|
|
||||||
```bash
|
### Android App
|
||||||
cd server
|
|
||||||
cp .env.example .env
|
|
||||||
nano .env # Passwort anpassen
|
|
||||||
docker-compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Server testen
|
**Option 1: APK herunterladen**
|
||||||
|
|
||||||
```bash
|
1. Neueste [Release](../../releases/latest) öffnen
|
||||||
curl -u noteuser:your_password http://localhost:8080/
|
2. `app-debug.apk` herunterladen
|
||||||
```
|
3. APK auf dem Handy installieren
|
||||||
|
|
||||||
### 3. Android App entwickeln
|
**Option 2: Selbst bauen**
|
||||||
|
|
||||||
```bash
|
|
||||||
cd android
|
|
||||||
# In Android Studio öffnen
|
|
||||||
# Build & Run
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📖 Dokumentation
|
|
||||||
|
|
||||||
### In diesem Repository:
|
|
||||||
|
|
||||||
- **[QUICKSTART.md](QUICKSTART.md)** - Schnellstart-Anleitung
|
|
||||||
- **[server/README.md](server/README.md)** - Server-Verwaltung
|
|
||||||
|
|
||||||
### Vollständige Dokumentation (project-docs):
|
|
||||||
|
|
||||||
- [README.md](https://github.com/inventory69/project-docs/blob/main/simple-notes-sync/README.md) - Projekt-Übersicht & Architektur
|
|
||||||
- [IMPLEMENTATION_PLAN.md](https://github.com/inventory69/project-docs/blob/main/simple-notes-sync/IMPLEMENTATION_PLAN.md) - Detaillierter Sprint-Plan
|
|
||||||
- [SERVER_SETUP.md](https://github.com/inventory69/project-docs/blob/main/simple-notes-sync/SERVER_SETUP.md) - Server-Setup Details
|
|
||||||
- [ANDROID_GUIDE.md](https://github.com/inventory69/project-docs/blob/main/simple-notes-sync/ANDROID_GUIDE.md) - 📱 Kompletter Android-Code
|
|
||||||
- [NOTIFICATIONS.md](https://github.com/inventory69/project-docs/blob/main/simple-notes-sync/NOTIFICATIONS.md) - Notification-System Details
|
|
||||||
- [WINDOWS_SETUP.md](https://github.com/inventory69/project-docs/blob/main/simple-notes-sync/WINDOWS_SETUP.md) - 🪟 Windows + Android Studio Setup
|
|
||||||
- [CODE_REFERENCE.md](https://github.com/inventory69/project-docs/blob/main/simple-notes-sync/CODE_REFERENCE.md) - Schnelle Code-Referenz
|
|
||||||
|
|
||||||
## ⚙️ Server Konfiguration
|
|
||||||
|
|
||||||
**Standard-Credentials:**
|
|
||||||
- Username: `noteuser`
|
|
||||||
- Password: Siehe `.env` im `server/` Verzeichnis
|
|
||||||
|
|
||||||
**Server-URL:**
|
|
||||||
- Lokal: `http://localhost:8080/`
|
|
||||||
- Im Netzwerk: `http://YOUR_IP:8080/`
|
|
||||||
|
|
||||||
IP-Adresse finden:
|
|
||||||
```bash
|
|
||||||
ip addr show | grep "inet " | grep -v 127.0.0.1
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📱 Android App Setup
|
|
||||||
|
|
||||||
### Vorraussetzungen
|
|
||||||
|
|
||||||
- Android Studio Hedgehog (2023.1.1) oder neuer
|
|
||||||
- JDK 17
|
|
||||||
- Min SDK 24 (Android 7.0)
|
|
||||||
- Target SDK 34 (Android 14)
|
|
||||||
|
|
||||||
### In App konfigurieren
|
|
||||||
|
|
||||||
1. App starten
|
|
||||||
2. Einstellungen öffnen
|
|
||||||
3. Server-URL eintragen (z.B. `http://192.168.1.100:8080/`)
|
|
||||||
4. Username & Passwort eingeben
|
|
||||||
5. Heim-WLAN SSID eingeben
|
|
||||||
6. "Verbindung testen"
|
|
||||||
|
|
||||||
## 🔧 Entwicklung
|
|
||||||
|
|
||||||
### Server-Management
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Status prüfen
|
|
||||||
docker-compose ps
|
|
||||||
|
|
||||||
# Logs anschauen
|
|
||||||
docker-compose logs -f
|
|
||||||
|
|
||||||
# Neustarten
|
|
||||||
docker-compose restart
|
|
||||||
|
|
||||||
# Stoppen
|
|
||||||
docker-compose down
|
|
||||||
```
|
|
||||||
|
|
||||||
### Android-Build
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd android
|
cd android
|
||||||
./gradlew assembleDebug
|
./gradlew assembleDebug
|
||||||
|
# APK: android/app/build/outputs/apk/debug/app-debug.apk
|
||||||
# APK Location:
|
|
||||||
# app/build/outputs/apk/debug/app-debug.apk
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🧪 Testing
|
### WebDAV Server
|
||||||
|
|
||||||
### Server-Test
|
Der Server läuft als Docker-Container und speichert deine Notizen.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Testdatei hochladen
|
cd server
|
||||||
echo '{"id":"test","title":"Test","content":"Hello"}' > test.json
|
cp .env.example .env
|
||||||
curl -u noteuser:password -T test.json http://localhost:8080/test.json
|
nano .env # Passwort anpassen!
|
||||||
|
|
||||||
# Datei abrufen
|
|
||||||
curl -u noteuser:password http://localhost:8080/test.json
|
|
||||||
|
|
||||||
# Datei löschen
|
|
||||||
curl -u noteuser:password -X DELETE http://localhost:8080/test.json
|
|
||||||
```
|
|
||||||
|
|
||||||
### Android-App
|
|
||||||
|
|
||||||
1. Notiz erstellen → speichern → in Liste sichtbar ✓
|
|
||||||
2. WLAN verbinden → Auto-Sync ✓
|
|
||||||
3. Server offline → Fehlermeldung ✓
|
|
||||||
4. Konflikt-Szenario → Auflösung ✓
|
|
||||||
|
|
||||||
## 📦 Deployment
|
|
||||||
|
|
||||||
### Server (Production)
|
|
||||||
|
|
||||||
**Option 1: Lokaler Server (Raspberry Pi, etc.)**
|
|
||||||
```bash
|
|
||||||
docker-compose up -d
|
docker-compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
**Option 2: VPS (DigitalOcean, Hetzner, etc.)**
|
**Server testen:**
|
||||||
```bash
|
```bash
|
||||||
# Mit HTTPS (empfohlen)
|
curl -u noteuser:dein_passwort http://192.168.0.XXX:8080/
|
||||||
# Zusätzlich: Reverse Proxy (nginx/Caddy) + Let's Encrypt
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Android App
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Release Build
|
|
||||||
./gradlew assembleRelease
|
|
||||||
|
|
||||||
# APK signieren
|
|
||||||
# Play Store Upload oder Direct Install
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔐 Security
|
|
||||||
|
|
||||||
**Entwicklung:**
|
|
||||||
- ✅ HTTP Basic Auth
|
|
||||||
- ✅ Nur im lokalen Netzwerk
|
|
||||||
|
|
||||||
**Produktion:**
|
|
||||||
- ⚠️ HTTPS mit SSL/TLS (empfohlen)
|
|
||||||
- ⚠️ Starkes Passwort
|
|
||||||
- ⚠️ Firewall-Regeln
|
|
||||||
- ⚠️ VPN für externen Zugriff
|
|
||||||
|
|
||||||
## 🐛 Troubleshooting
|
|
||||||
|
|
||||||
### Server startet nicht
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Port bereits belegt?
|
|
||||||
sudo netstat -tlnp | grep 8080
|
|
||||||
|
|
||||||
# Logs checken
|
|
||||||
docker-compose logs webdav
|
|
||||||
```
|
|
||||||
|
|
||||||
### Android kann nicht verbinden
|
|
||||||
|
|
||||||
- Ist Android im gleichen WLAN?
|
|
||||||
- Ist die Server-IP korrekt?
|
|
||||||
- Firewall blockiert Port 8080?
|
|
||||||
- Credentials korrekt?
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Ping zum Server
|
|
||||||
ping YOUR_SERVER_IP
|
|
||||||
|
|
||||||
# Port erreichbar?
|
|
||||||
telnet YOUR_SERVER_IP 8080
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📝 TODO / Roadmap
|
|
||||||
|
|
||||||
### Version 1.0 (MVP)
|
|
||||||
- [x] Docker WebDAV Server
|
|
||||||
- [ ] Android Basic CRUD
|
|
||||||
- [ ] Auto-Sync bei WLAN
|
|
||||||
- [ ] Error Handling
|
|
||||||
- [ ] Notifications
|
|
||||||
|
|
||||||
### Version 1.1
|
|
||||||
- [ ] Suche
|
|
||||||
- [ ] Dark Mode
|
|
||||||
- [ ] Markdown-Support
|
|
||||||
|
|
||||||
### Version 2.0
|
|
||||||
- [ ] Desktop-Client (Flutter Desktop)
|
|
||||||
- [ ] Tags/Kategorien
|
|
||||||
- [ ] Verschlüsselung
|
|
||||||
- [ ] Shared Notes
|
|
||||||
|
|
||||||
## 📄 License
|
|
||||||
|
|
||||||
MIT License - siehe [LICENSE](LICENSE)
|
|
||||||
|
|
||||||
## 👤 Author
|
|
||||||
|
|
||||||
Created for personal use - 2025
|
|
||||||
|
|
||||||
## 🙏 Acknowledgments
|
|
||||||
|
|
||||||
- [bytemark/webdav](https://hub.docker.com/r/bytemark/webdav) - Docker WebDAV Server
|
|
||||||
- [Sardine Android](https://github.com/thegrizzlylabs/sardine-android) - WebDAV Client
|
|
||||||
- [Android WorkManager](https://developer.android.com/topic/libraries/architecture/workmanager) - Background Tasks
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Project Start:** 19. Dezember 2025
|
## 🚀 Schnellstart
|
||||||
**Status:** 🚧 In Development
|
|
||||||
|
1. **Server starten** (siehe oben)
|
||||||
|
2. **App installieren** und öffnen
|
||||||
|
3. **Einstellungen öffnen** (⚙️ Symbol oben rechts)
|
||||||
|
4. **Server konfigurieren:**
|
||||||
|
- Server-URL: `http://192.168.0.XXX:8080/notes`
|
||||||
|
- Benutzername: `noteuser`
|
||||||
|
- Passwort: (aus `.env` Datei)
|
||||||
|
- Auto-Sync: **AN**
|
||||||
|
5. **Fertig!** Notizen werden jetzt automatisch synchronisiert
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 Wie funktioniert Auto-Sync?
|
||||||
|
|
||||||
|
Die App prüft **alle 30 Minuten**, ob:
|
||||||
|
- ✅ WLAN verbunden ist
|
||||||
|
- ✅ Server im gleichen Netzwerk erreichbar ist
|
||||||
|
- ✅ Neue Notizen vorhanden sind
|
||||||
|
|
||||||
|
Wenn alle Bedingungen erfüllt → **Automatische Synchronisierung**
|
||||||
|
|
||||||
|
**Wichtig:** Funktioniert nur im selben Netzwerk wie der Server (kein Internet-Zugriff nötig!)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔋 Akkuverbrauch
|
||||||
|
|
||||||
|
| Komponente | Verbrauch/Tag |
|
||||||
|
|------------|---------------|
|
||||||
|
| WorkManager (alle 30 Min) | ~0.3% |
|
||||||
|
| Netzwerk-Checks | ~0.1% |
|
||||||
|
| **Total** | **~0.4%** |
|
||||||
|
|
||||||
|
Bei einem 3000 mAh Akku entspricht das ~12 mAh pro Tag.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📱 Screenshots
|
||||||
|
|
||||||
|
_TODO: Screenshots hinzufügen_
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ Technische Details
|
||||||
|
|
||||||
|
Mehr Infos zur Architektur und Implementierung findest du in der [technischen Dokumentation](DOCS.md).
|
||||||
|
|
||||||
|
**Stack:**
|
||||||
|
- **Android:** Kotlin, Material Design 3, WorkManager
|
||||||
|
- **Server:** Docker, WebDAV (bytemark/webdav)
|
||||||
|
- **Sync:** Sardine Android (WebDAV Client)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🐛 Troubleshooting
|
||||||
|
|
||||||
|
### Server nicht erreichbar
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Server Status prüfen
|
||||||
|
docker-compose ps
|
||||||
|
|
||||||
|
# Logs ansehen
|
||||||
|
docker-compose logs -f
|
||||||
|
|
||||||
|
# IP-Adresse finden
|
||||||
|
ip addr show | grep "inet " | grep -v 127.0.0.1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Auto-Sync funktioniert nicht
|
||||||
|
|
||||||
|
1. **Akku-Optimierung deaktivieren**
|
||||||
|
- Einstellungen → Apps → Simple Notes → Akku → Nicht optimieren
|
||||||
|
2. **WLAN Verbindung prüfen**
|
||||||
|
- App funktioniert nur im selben Netzwerk wie der Server
|
||||||
|
3. **Server-Status in App prüfen**
|
||||||
|
- Settings → Server-Status sollte "Erreichbar" zeigen
|
||||||
|
|
||||||
|
Mehr Details in der [Dokumentation](DOCS.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🤝 Beitragen
|
||||||
|
|
||||||
|
Contributions sind willkommen! Bitte öffne ein Issue oder Pull Request.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📄 Lizenz
|
||||||
|
|
||||||
|
MIT License - siehe [LICENSE](LICENSE)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Projekt Start:** 19. Dezember 2025
|
||||||
|
**Status:** ✅ Funktional & Produktiv
|
||||||
|
|||||||
335
README.old.md
Normal file
335
README.old.md
Normal file
@@ -0,0 +1,335 @@
|
|||||||
|
# Simple Notes Sync 📝
|
||||||
|
|
||||||
|
> Minimalistische Android-App für Offline-Notizen mit automatischer WLAN-Synchronisierung
|
||||||
|
|
||||||
|
Eine schlanke Notiz-App ohne Schnickschnack - perfekt für schnelle Gedanken, die automatisch zu Hause synchronisiert werden.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✨ Features
|
||||||
|
|
||||||
|
- 📝 **Offline-first** - Notizen werden lokal gespeichert und sind immer verfügbar
|
||||||
|
- 🔄 **Auto-Sync** - Automatische Synchronisierung wenn du im Heimnetzwerk bist
|
||||||
|
- 🏠 **WebDAV Server** - Deine Daten bleiben bei dir (Docker-Container)
|
||||||
|
- 🔋 **Akkuschonend** - Nur ~0.4% Akkuverbrauch pro Tag
|
||||||
|
- 🚫 **Keine Cloud** - Keine Google, keine Microsoft, keine Drittanbieter
|
||||||
|
- 🔐 **Privacy** - Keine Tracking, keine Analytics, keine Standort-Berechtigungen
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📥 Installation
|
||||||
|
|
||||||
|
### Android App
|
||||||
|
|
||||||
|
**Option 1: APK herunterladen**
|
||||||
|
|
||||||
|
1. Neueste [Release](../../releases/latest) öffnen
|
||||||
|
2. `app-debug.apk` herunterladen
|
||||||
|
3. APK auf dem Handy installieren
|
||||||
|
|
||||||
|
**Option 2: Selbst bauen**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd android
|
||||||
|
./gradlew assembleDebug
|
||||||
|
# APK: android/app/build/outputs/apk/debug/app-debug.apk
|
||||||
|
```
|
||||||
|
|
||||||
|
### WebDAV Server
|
||||||
|
|
||||||
|
Der Server läuft als Docker-Container und speichert deine Notizen.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd server
|
||||||
|
cp .env.example .env
|
||||||
|
nano .env # Passwort anpassen!
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
**Server testen:**
|
||||||
|
```bash
|
||||||
|
curl -u noteuser:dein_passwort http://192.168.0.XXX:8080/
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Schnellstart
|
||||||
|
|
||||||
|
1. **Server starten** (siehe oben)
|
||||||
|
2. **App installieren** und öffnen
|
||||||
|
3. **Einstellungen öffnen** (⚙️ Symbol oben rechts)
|
||||||
|
4. **Server konfigurieren:**
|
||||||
|
- Server-URL: `http://192.168.0.XXX:8080/notes`
|
||||||
|
- Benutzername: `noteuser`
|
||||||
|
- Passwort: (aus `.env` Datei)
|
||||||
|
- Auto-Sync: **AN**
|
||||||
|
5. **Fertig!** Notizen werden jetzt automatisch synchronisiert
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 Wie funktioniert Auto-Sync?
|
||||||
|
|
||||||
|
Die App prüft **alle 30 Minuten**, ob:
|
||||||
|
- ✅ WLAN verbunden ist
|
||||||
|
- ✅ Server im gleichen Netzwerk erreichbar ist
|
||||||
|
- ✅ Neue Notizen vorhanden sind
|
||||||
|
|
||||||
|
Wenn alle Bedingungen erfüllt → **Automatische Synchronisierung**
|
||||||
|
|
||||||
|
**Wichtig:** Funktioniert nur im selben Netzwerk wie der Server (kein Internet-Zugriff nötig!)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔋 Akkuverbrauch
|
||||||
|
|
||||||
|
| Komponente | Verbrauch/Tag |
|
||||||
|
|------------|---------------|
|
||||||
|
| WorkManager (alle 30 Min) | ~0.3% |
|
||||||
|
| Netzwerk-Checks | ~0.1% |
|
||||||
|
| **Total** | **~0.4%** |
|
||||||
|
|
||||||
|
Bei einem 3000 mAh Akku entspricht das ~12 mAh pro Tag.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📱 Screenshots
|
||||||
|
|
||||||
|
_TODO: Screenshots hinzufügen_
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ Technische Details
|
||||||
|
|
||||||
|
Mehr Infos zur Architektur und Implementierung findest du in der [technischen Dokumentation](DOCS.md).
|
||||||
|
|
||||||
|
**Stack:**
|
||||||
|
- **Android:** Kotlin, Material Design 3, WorkManager
|
||||||
|
- **Server:** Docker, WebDAV (bytemark/webdav)
|
||||||
|
- **Sync:** Sardine Android (WebDAV Client)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📄 Lizenz
|
||||||
|
|
||||||
|
[Lizenz hier einfügen]
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🤝 Beitragen
|
||||||
|
|
||||||
|
Contributions sind willkommen! Bitte öffne ein Issue oder Pull Request.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📄 Lizenz
|
||||||
|
|
||||||
|
MIT License - siehe [LICENSE](LICENSE)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Projekt Start:** 19. Dezember 2025
|
||||||
|
**Status:** ✅ Funktional & Produktiv
|
||||||
|
|
||||||
|
## 📖 Dokumentation
|
||||||
|
|
||||||
|
### In diesem Repository:
|
||||||
|
|
||||||
|
- **[QUICKSTART.md](QUICKSTART.md)** - Schnellstart-Anleitung
|
||||||
|
- **[server/README.md](server/README.md)** - Server-Verwaltung
|
||||||
|
|
||||||
|
### Vollständige Dokumentation (project-docs):
|
||||||
|
|
||||||
|
- [README.md](https://github.com/inventory69/project-docs/blob/main/simple-notes-sync/README.md) - Projekt-Übersicht & Architektur
|
||||||
|
- [IMPLEMENTATION_PLAN.md](https://github.com/inventory69/project-docs/blob/main/simple-notes-sync/IMPLEMENTATION_PLAN.md) - Detaillierter Sprint-Plan
|
||||||
|
- [SERVER_SETUP.md](https://github.com/inventory69/project-docs/blob/main/simple-notes-sync/SERVER_SETUP.md) - Server-Setup Details
|
||||||
|
- [ANDROID_GUIDE.md](https://github.com/inventory69/project-docs/blob/main/simple-notes-sync/ANDROID_GUIDE.md) - 📱 Kompletter Android-Code
|
||||||
|
- [NOTIFICATIONS.md](https://github.com/inventory69/project-docs/blob/main/simple-notes-sync/NOTIFICATIONS.md) - Notification-System Details
|
||||||
|
- [WINDOWS_SETUP.md](https://github.com/inventory69/project-docs/blob/main/simple-notes-sync/WINDOWS_SETUP.md) - 🪟 Windows + Android Studio Setup
|
||||||
|
- [CODE_REFERENCE.md](https://github.com/inventory69/project-docs/blob/main/simple-notes-sync/CODE_REFERENCE.md) - Schnelle Code-Referenz
|
||||||
|
|
||||||
|
## ⚙️ Server Konfiguration
|
||||||
|
|
||||||
|
**Standard-Credentials:**
|
||||||
|
- Username: `noteuser`
|
||||||
|
- Password: Siehe `.env` im `server/` Verzeichnis
|
||||||
|
|
||||||
|
**Server-URL:**
|
||||||
|
- Lokal: `http://localhost:8080/`
|
||||||
|
- Im Netzwerk: `http://YOUR_IP:8080/`
|
||||||
|
|
||||||
|
IP-Adresse finden:
|
||||||
|
```bash
|
||||||
|
ip addr show | grep "inet " | grep -v 127.0.0.1
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📱 Android App Setup
|
||||||
|
|
||||||
|
### Vorraussetzungen
|
||||||
|
|
||||||
|
- Android Studio Hedgehog (2023.1.1) oder neuer
|
||||||
|
- JDK 17
|
||||||
|
- Min SDK 24 (Android 7.0)
|
||||||
|
- Target SDK 34 (Android 14)
|
||||||
|
|
||||||
|
### In App konfigurieren
|
||||||
|
|
||||||
|
1. App starten
|
||||||
|
2. Einstellungen öffnen
|
||||||
|
3. Server-URL eintragen (z.B. `http://192.168.1.100:8080/`)
|
||||||
|
4. Username & Passwort eingeben
|
||||||
|
5. Heim-WLAN SSID eingeben
|
||||||
|
6. "Verbindung testen"
|
||||||
|
|
||||||
|
## 🔧 Entwicklung
|
||||||
|
|
||||||
|
### Server-Management
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Status prüfen
|
||||||
|
docker-compose ps
|
||||||
|
|
||||||
|
# Logs anschauen
|
||||||
|
docker-compose logs -f
|
||||||
|
|
||||||
|
# Neustarten
|
||||||
|
docker-compose restart
|
||||||
|
|
||||||
|
# Stoppen
|
||||||
|
docker-compose down
|
||||||
|
```
|
||||||
|
|
||||||
|
### Android-Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd android
|
||||||
|
./gradlew assembleDebug
|
||||||
|
|
||||||
|
# APK Location:
|
||||||
|
# app/build/outputs/apk/debug/app-debug.apk
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🧪 Testing
|
||||||
|
|
||||||
|
### Server-Test
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Testdatei hochladen
|
||||||
|
echo '{"id":"test","title":"Test","content":"Hello"}' > test.json
|
||||||
|
curl -u noteuser:password -T test.json http://localhost:8080/test.json
|
||||||
|
|
||||||
|
# Datei abrufen
|
||||||
|
curl -u noteuser:password http://localhost:8080/test.json
|
||||||
|
|
||||||
|
# Datei löschen
|
||||||
|
curl -u noteuser:password -X DELETE http://localhost:8080/test.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### Android-App
|
||||||
|
|
||||||
|
1. Notiz erstellen → speichern → in Liste sichtbar ✓
|
||||||
|
2. WLAN verbinden → Auto-Sync ✓
|
||||||
|
3. Server offline → Fehlermeldung ✓
|
||||||
|
4. Konflikt-Szenario → Auflösung ✓
|
||||||
|
|
||||||
|
## 📦 Deployment
|
||||||
|
|
||||||
|
### Server (Production)
|
||||||
|
|
||||||
|
**Option 1: Lokaler Server (Raspberry Pi, etc.)**
|
||||||
|
```bash
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
**Option 2: VPS (DigitalOcean, Hetzner, etc.)**
|
||||||
|
```bash
|
||||||
|
# Mit HTTPS (empfohlen)
|
||||||
|
# Zusätzlich: Reverse Proxy (nginx/Caddy) + Let's Encrypt
|
||||||
|
```
|
||||||
|
|
||||||
|
### Android App
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Release Build
|
||||||
|
./gradlew assembleRelease
|
||||||
|
|
||||||
|
# APK signieren
|
||||||
|
# Play Store Upload oder Direct Install
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔐 Security
|
||||||
|
|
||||||
|
**Entwicklung:**
|
||||||
|
- ✅ HTTP Basic Auth
|
||||||
|
- ✅ Nur im lokalen Netzwerk
|
||||||
|
|
||||||
|
**Produktion:**
|
||||||
|
- ⚠️ HTTPS mit SSL/TLS (empfohlen)
|
||||||
|
- ⚠️ Starkes Passwort
|
||||||
|
- ⚠️ Firewall-Regeln
|
||||||
|
- ⚠️ VPN für externen Zugriff
|
||||||
|
|
||||||
|
## 🐛 Troubleshooting
|
||||||
|
|
||||||
|
### Server startet nicht
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Port bereits belegt?
|
||||||
|
sudo netstat -tlnp | grep 8080
|
||||||
|
|
||||||
|
# Logs checken
|
||||||
|
docker-compose logs webdav
|
||||||
|
```
|
||||||
|
|
||||||
|
### Android kann nicht verbinden
|
||||||
|
|
||||||
|
- Ist Android im gleichen WLAN?
|
||||||
|
- Ist die Server-IP korrekt?
|
||||||
|
- Firewall blockiert Port 8080?
|
||||||
|
- Credentials korrekt?
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Ping zum Server
|
||||||
|
ping YOUR_SERVER_IP
|
||||||
|
|
||||||
|
# Port erreichbar?
|
||||||
|
telnet YOUR_SERVER_IP 8080
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📝 TODO / Roadmap
|
||||||
|
|
||||||
|
### Version 1.0 (MVP)
|
||||||
|
- [x] Docker WebDAV Server
|
||||||
|
- [ ] Android Basic CRUD
|
||||||
|
- [ ] Auto-Sync bei WLAN
|
||||||
|
- [ ] Error Handling
|
||||||
|
- [ ] Notifications
|
||||||
|
|
||||||
|
### Version 1.1
|
||||||
|
- [ ] Suche
|
||||||
|
- [ ] Dark Mode
|
||||||
|
- [ ] Markdown-Support
|
||||||
|
|
||||||
|
### Version 2.0
|
||||||
|
- [ ] Desktop-Client (Flutter Desktop)
|
||||||
|
- [ ] Tags/Kategorien
|
||||||
|
- [ ] Verschlüsselung
|
||||||
|
- [ ] Shared Notes
|
||||||
|
|
||||||
|
## 📄 License
|
||||||
|
|
||||||
|
MIT License - siehe [LICENSE](LICENSE)
|
||||||
|
|
||||||
|
## 👤 Author
|
||||||
|
|
||||||
|
Created for personal use - 2025
|
||||||
|
|
||||||
|
## 🙏 Acknowledgments
|
||||||
|
|
||||||
|
- [bytemark/webdav](https://hub.docker.com/r/bytemark/webdav) - Docker WebDAV Server
|
||||||
|
- [Sardine Android](https://github.com/thegrizzlylabs/sardine-android) - WebDAV Client
|
||||||
|
- [Android WorkManager](https://developer.android.com/topic/libraries/architecture/workmanager) - Background Tasks
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Project Start:** 19. Dezember 2025
|
||||||
|
**Status:** 🚧 In Development
|
||||||
@@ -17,20 +17,55 @@ android {
|
|||||||
versionName = "1.0"
|
versionName = "1.0"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
|
||||||
|
// Enable multiple APKs per ABI for smaller downloads
|
||||||
|
splits {
|
||||||
|
abi {
|
||||||
|
isEnable = true
|
||||||
|
reset()
|
||||||
|
include("armeabi-v7a", "arm64-v8a")
|
||||||
|
isUniversalApk = true // Also generate universal APK
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Signing configuration for release builds
|
||||||
|
signingConfigs {
|
||||||
|
create("release") {
|
||||||
|
// Load keystore configuration from key.properties file
|
||||||
|
val keystorePropertiesFile = rootProject.file("key.properties")
|
||||||
|
if (keystorePropertiesFile.exists()) {
|
||||||
|
val keystoreProperties = java.util.Properties()
|
||||||
|
keystoreProperties.load(java.io.FileInputStream(keystorePropertiesFile))
|
||||||
|
|
||||||
|
storeFile = file(keystoreProperties["storeFile"] as String)
|
||||||
|
storePassword = keystoreProperties["storePassword"] as String
|
||||||
|
keyAlias = keystoreProperties["keyAlias"] as String
|
||||||
|
keyPassword = keystoreProperties["keyPassword"] as String
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
isMinifyEnabled = false
|
isMinifyEnabled = true
|
||||||
|
isShrinkResources = true
|
||||||
proguardFiles(
|
proguardFiles(
|
||||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||||
"proguard-rules.pro"
|
"proguard-rules.pro"
|
||||||
)
|
)
|
||||||
|
// Use release signing config if available, otherwise debug
|
||||||
|
signingConfig = if (rootProject.file("key.properties").exists()) {
|
||||||
|
signingConfigs.getByName("release")
|
||||||
|
} else {
|
||||||
|
signingConfigs.getByName("debug")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
viewBinding = true
|
viewBinding = true
|
||||||
|
buildConfig = true // Enable BuildConfig generation
|
||||||
}
|
}
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
@@ -59,6 +94,9 @@ dependencies {
|
|||||||
implementation("androidx.recyclerview:recyclerview:1.3.2")
|
implementation("androidx.recyclerview:recyclerview:1.3.2")
|
||||||
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0")
|
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0")
|
||||||
|
|
||||||
|
// LocalBroadcastManager für UI Refresh
|
||||||
|
implementation("androidx.localbroadcastmanager:localbroadcastmanager:1.1.0")
|
||||||
|
|
||||||
// Testing (bleiben so)
|
// Testing (bleiben so)
|
||||||
testImplementation(libs.junit)
|
testImplementation(libs.junit)
|
||||||
androidTestImplementation(libs.androidx.junit)
|
androidTestImplementation(libs.androidx.junit)
|
||||||
|
|||||||
@@ -2,16 +2,20 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
|
<!-- Network & Sync Permissions -->
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_WIFI_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.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" />
|
<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.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
|
<application
|
||||||
android:name=".SimpleNotesApplication"
|
android:name=".SimpleNotesApplication"
|
||||||
@@ -45,6 +49,17 @@
|
|||||||
android:name=".SettingsActivity"
|
android:name=".SettingsActivity"
|
||||||
android:parentActivityName=".MainActivity" />
|
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>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
@@ -1,19 +1,25 @@
|
|||||||
package dev.dettmer.simplenotes
|
package dev.dettmer.simplenotes
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import dev.dettmer.simplenotes.utils.Logger
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.google.android.material.appbar.MaterialToolbar
|
import com.google.android.material.appbar.MaterialToolbar
|
||||||
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||||
import dev.dettmer.simplenotes.adapters.NotesAdapter
|
import dev.dettmer.simplenotes.adapters.NotesAdapter
|
||||||
import dev.dettmer.simplenotes.storage.NotesStorage
|
import dev.dettmer.simplenotes.storage.NotesStorage
|
||||||
|
import dev.dettmer.simplenotes.sync.SyncWorker
|
||||||
import dev.dettmer.simplenotes.utils.NotificationHelper
|
import dev.dettmer.simplenotes.utils.NotificationHelper
|
||||||
import dev.dettmer.simplenotes.utils.showToast
|
import dev.dettmer.simplenotes.utils.showToast
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
@@ -34,9 +40,28 @@ class MainActivity : AppCompatActivity() {
|
|||||||
private val storage by lazy { NotesStorage(this) }
|
private val storage by lazy { NotesStorage(this) }
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
private const val TAG = "MainActivity"
|
||||||
private const val REQUEST_NOTIFICATION_PERMISSION = 1001
|
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?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(R.layout.activity_main)
|
setContentView(R.layout.activity_main)
|
||||||
@@ -56,9 +81,25 @@ class MainActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.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()
|
loadNotes()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
super.onPause()
|
||||||
|
|
||||||
|
// Unregister BroadcastReceiver
|
||||||
|
LocalBroadcastManager.getInstance(this).unregisterReceiver(syncCompletedReceiver)
|
||||||
|
Logger.d(TAG, "📡 BroadcastReceiver unregistered")
|
||||||
|
}
|
||||||
|
|
||||||
private fun findViews() {
|
private fun findViews() {
|
||||||
recyclerViewNotes = findViewById(R.id.recyclerViewNotes)
|
recyclerViewNotes = findViewById(R.id.recyclerViewNotes)
|
||||||
textViewEmpty = findViewById(R.id.textViewEmpty)
|
textViewEmpty = findViewById(R.id.textViewEmpty)
|
||||||
|
|||||||
@@ -1,50 +1,48 @@
|
|||||||
package dev.dettmer.simplenotes
|
package dev.dettmer.simplenotes
|
||||||
|
|
||||||
import android.Manifest
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.net.wifi.WifiManager
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.PowerManager
|
import android.os.PowerManager
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
|
import android.util.Log
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.widget.Button
|
import android.widget.Button
|
||||||
import android.widget.EditText
|
import android.widget.EditText
|
||||||
|
import android.widget.TextView
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.appcompat.widget.SwitchCompat
|
import androidx.appcompat.widget.SwitchCompat
|
||||||
import androidx.core.app.ActivityCompat
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.google.android.material.appbar.MaterialToolbar
|
import com.google.android.material.appbar.MaterialToolbar
|
||||||
import dev.dettmer.simplenotes.sync.WebDavSyncService
|
import dev.dettmer.simplenotes.sync.WebDavSyncService
|
||||||
import dev.dettmer.simplenotes.utils.Constants
|
import dev.dettmer.simplenotes.utils.Constants
|
||||||
import dev.dettmer.simplenotes.utils.showToast
|
import dev.dettmer.simplenotes.utils.showToast
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.net.HttpURLConnection
|
||||||
|
import java.net.URL
|
||||||
|
|
||||||
class SettingsActivity : AppCompatActivity() {
|
class SettingsActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "SettingsActivity"
|
||||||
|
}
|
||||||
|
|
||||||
private lateinit var editTextServerUrl: EditText
|
private lateinit var editTextServerUrl: EditText
|
||||||
private lateinit var editTextUsername: EditText
|
private lateinit var editTextUsername: EditText
|
||||||
private lateinit var editTextPassword: EditText
|
private lateinit var editTextPassword: EditText
|
||||||
private lateinit var editTextHomeSSID: EditText
|
|
||||||
private lateinit var switchAutoSync: SwitchCompat
|
private lateinit var switchAutoSync: SwitchCompat
|
||||||
private lateinit var buttonTestConnection: Button
|
private lateinit var buttonTestConnection: Button
|
||||||
private lateinit var buttonSyncNow: Button
|
private lateinit var buttonSyncNow: Button
|
||||||
private lateinit var buttonDetectSSID: Button
|
private lateinit var textViewServerStatus: TextView
|
||||||
|
|
||||||
private val prefs by lazy {
|
private val prefs by lazy {
|
||||||
getSharedPreferences(Constants.PREFS_NAME, MODE_PRIVATE)
|
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?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(R.layout.activity_settings)
|
setContentView(R.layout.activity_settings)
|
||||||
@@ -66,19 +64,20 @@ class SettingsActivity : AppCompatActivity() {
|
|||||||
editTextServerUrl = findViewById(R.id.editTextServerUrl)
|
editTextServerUrl = findViewById(R.id.editTextServerUrl)
|
||||||
editTextUsername = findViewById(R.id.editTextUsername)
|
editTextUsername = findViewById(R.id.editTextUsername)
|
||||||
editTextPassword = findViewById(R.id.editTextPassword)
|
editTextPassword = findViewById(R.id.editTextPassword)
|
||||||
editTextHomeSSID = findViewById(R.id.editTextHomeSSID)
|
|
||||||
switchAutoSync = findViewById(R.id.switchAutoSync)
|
switchAutoSync = findViewById(R.id.switchAutoSync)
|
||||||
buttonTestConnection = findViewById(R.id.buttonTestConnection)
|
buttonTestConnection = findViewById(R.id.buttonTestConnection)
|
||||||
buttonSyncNow = findViewById(R.id.buttonSyncNow)
|
buttonSyncNow = findViewById(R.id.buttonSyncNow)
|
||||||
buttonDetectSSID = findViewById(R.id.buttonDetectSSID)
|
textViewServerStatus = findViewById(R.id.textViewServerStatus)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadSettings() {
|
private fun loadSettings() {
|
||||||
editTextServerUrl.setText(prefs.getString(Constants.KEY_SERVER_URL, ""))
|
editTextServerUrl.setText(prefs.getString(Constants.KEY_SERVER_URL, ""))
|
||||||
editTextUsername.setText(prefs.getString(Constants.KEY_USERNAME, ""))
|
editTextUsername.setText(prefs.getString(Constants.KEY_USERNAME, ""))
|
||||||
editTextPassword.setText(prefs.getString(Constants.KEY_PASSWORD, ""))
|
editTextPassword.setText(prefs.getString(Constants.KEY_PASSWORD, ""))
|
||||||
editTextHomeSSID.setText(prefs.getString(Constants.KEY_HOME_SSID, ""))
|
|
||||||
switchAutoSync.isChecked = prefs.getBoolean(Constants.KEY_AUTO_SYNC, false)
|
switchAutoSync.isChecked = prefs.getBoolean(Constants.KEY_AUTO_SYNC, false)
|
||||||
|
|
||||||
|
// Server Status prüfen
|
||||||
|
checkServerStatus()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupListeners() {
|
private fun setupListeners() {
|
||||||
@@ -92,13 +91,16 @@ class SettingsActivity : AppCompatActivity() {
|
|||||||
syncNow()
|
syncNow()
|
||||||
}
|
}
|
||||||
|
|
||||||
buttonDetectSSID.setOnClickListener {
|
|
||||||
detectCurrentSSID()
|
|
||||||
}
|
|
||||||
|
|
||||||
switchAutoSync.setOnCheckedChangeListener { _, isChecked ->
|
switchAutoSync.setOnCheckedChangeListener { _, isChecked ->
|
||||||
onAutoSyncToggled(isChecked)
|
onAutoSyncToggled(isChecked)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Server Status Check bei Settings-Änderung
|
||||||
|
editTextServerUrl.setOnFocusChangeListener { _, hasFocus ->
|
||||||
|
if (!hasFocus) {
|
||||||
|
checkServerStatus()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun saveSettings() {
|
private fun saveSettings() {
|
||||||
@@ -106,7 +108,6 @@ class SettingsActivity : AppCompatActivity() {
|
|||||||
putString(Constants.KEY_SERVER_URL, editTextServerUrl.text.toString().trim())
|
putString(Constants.KEY_SERVER_URL, editTextServerUrl.text.toString().trim())
|
||||||
putString(Constants.KEY_USERNAME, editTextUsername.text.toString().trim())
|
putString(Constants.KEY_USERNAME, editTextUsername.text.toString().trim())
|
||||||
putString(Constants.KEY_PASSWORD, editTextPassword.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)
|
putBoolean(Constants.KEY_AUTO_SYNC, switchAutoSync.isChecked)
|
||||||
apply()
|
apply()
|
||||||
}
|
}
|
||||||
@@ -152,34 +153,41 @@ class SettingsActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun detectCurrentSSID() {
|
private fun checkServerStatus() {
|
||||||
// Check if we have location permission (needed for SSID on Android 10+)
|
val serverUrl = prefs.getString(Constants.KEY_SERVER_URL, null)
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
||||||
if (ContextCompat.checkSelfPermission(
|
if (serverUrl.isNullOrEmpty()) {
|
||||||
this,
|
textViewServerStatus.text = "❌ Nicht konfiguriert"
|
||||||
Manifest.permission.ACCESS_FINE_LOCATION
|
textViewServerStatus.setTextColor(getColor(android.R.color.holo_red_dark))
|
||||||
) != PackageManager.PERMISSION_GRANTED
|
return
|
||||||
) {
|
|
||||||
// Request permission
|
|
||||||
ActivityCompat.requestPermissions(
|
|
||||||
this,
|
|
||||||
arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
|
|
||||||
REQUEST_LOCATION_PERMISSION
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Permission granted, get SSID
|
textViewServerStatus.text = "🔍 Prüfe..."
|
||||||
val wifiManager = applicationContext.getSystemService(WIFI_SERVICE) as WifiManager
|
textViewServerStatus.setTextColor(getColor(android.R.color.darker_gray))
|
||||||
val wifiInfo = wifiManager.connectionInfo
|
|
||||||
val ssid = wifiInfo.ssid.replace("\"", "")
|
|
||||||
|
|
||||||
if (ssid.isNotEmpty() && ssid != "<unknown ssid>") {
|
lifecycleScope.launch {
|
||||||
editTextHomeSSID.setText(ssid)
|
val isReachable = withContext(Dispatchers.IO) {
|
||||||
showToast("SSID erkannt: $ssid")
|
try {
|
||||||
} else {
|
val url = URL(serverUrl)
|
||||||
showToast("Nicht mit WLAN verbunden")
|
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) {
|
if (enabled) {
|
||||||
showToast("Auto-Sync aktiviert")
|
showToast("Auto-Sync aktiviert")
|
||||||
// Check battery optimization when enabling
|
|
||||||
checkBatteryOptimization()
|
checkBatteryOptimization()
|
||||||
// Check background location permission (needed for SSID on Android 12+)
|
restartNetworkMonitor()
|
||||||
checkBackgroundLocationPermission()
|
|
||||||
} else {
|
} else {
|
||||||
showToast("Auto-Sync deaktiviert")
|
showToast("Auto-Sync deaktiviert")
|
||||||
|
restartNetworkMonitor()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -240,99 +247,16 @@ class SettingsActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun checkBackgroundLocationPermission() {
|
private fun restartNetworkMonitor() {
|
||||||
// Background location permission only needed on Android 10+
|
try {
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
val app = application as SimpleNotesApplication
|
||||||
return
|
Log.d(TAG, "🔄 Restarting NetworkMonitor with new settings")
|
||||||
}
|
app.networkMonitor.stopMonitoring()
|
||||||
|
app.networkMonitor.startMonitoring()
|
||||||
// First check if we have foreground location
|
Log.d(TAG, "✅ NetworkMonitor restarted successfully")
|
||||||
if (ContextCompat.checkSelfPermission(
|
} catch (e: Exception) {
|
||||||
this,
|
Log.e(TAG, "❌ Failed to restart NetworkMonitor", e)
|
||||||
Manifest.permission.ACCESS_FINE_LOCATION
|
showToast("Fehler beim Neustart des NetworkMonitors")
|
||||||
) != 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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package dev.dettmer.simplenotes
|
package dev.dettmer.simplenotes
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.util.Log
|
import dev.dettmer.simplenotes.utils.Logger
|
||||||
import dev.dettmer.simplenotes.sync.NetworkMonitor
|
import dev.dettmer.simplenotes.sync.NetworkMonitor
|
||||||
import dev.dettmer.simplenotes.utils.NotificationHelper
|
import dev.dettmer.simplenotes.utils.NotificationHelper
|
||||||
|
|
||||||
@@ -11,31 +11,34 @@ class SimpleNotesApplication : Application() {
|
|||||||
private const val TAG = "SimpleNotesApp"
|
private const val TAG = "SimpleNotesApp"
|
||||||
}
|
}
|
||||||
|
|
||||||
private lateinit var networkMonitor: NetworkMonitor
|
lateinit var networkMonitor: NetworkMonitor // Public access für SettingsActivity
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
|
||||||
Log.d(TAG, "🚀 Application onCreate()")
|
Logger.d(TAG, "🚀 Application onCreate()")
|
||||||
|
|
||||||
// Initialize notification channel
|
// Initialize notification channel
|
||||||
NotificationHelper.createNotificationChannel(this)
|
NotificationHelper.createNotificationChannel(this)
|
||||||
Log.d(TAG, "✅ Notification channel created")
|
Logger.d(TAG, "✅ Notification channel created")
|
||||||
|
|
||||||
// Initialize and start NetworkMonitor at application level
|
// Initialize NetworkMonitor (WorkManager-based)
|
||||||
// CRITICAL: Use applicationContext, not 'this'!
|
// VORTEIL: WorkManager läuft auch ohne aktive App!
|
||||||
networkMonitor = NetworkMonitor(applicationContext)
|
networkMonitor = NetworkMonitor(applicationContext)
|
||||||
|
|
||||||
|
// Start WorkManager periodic sync
|
||||||
|
// Dies läuft im Hintergrund auch wenn App geschlossen ist
|
||||||
networkMonitor.startMonitoring()
|
networkMonitor.startMonitoring()
|
||||||
|
|
||||||
Log.d(TAG, "✅ NetworkMonitor initialized and started")
|
Logger.d(TAG, "✅ WorkManager-based auto-sync initialized")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onTerminate() {
|
override fun onTerminate() {
|
||||||
super.onTerminate()
|
super.onTerminate()
|
||||||
|
|
||||||
Log.d(TAG, "🛑 Application onTerminate()")
|
Logger.d(TAG, "🛑 Application onTerminate()")
|
||||||
|
|
||||||
// Clean up NetworkMonitor when app is terminated
|
// WorkManager läuft weiter auch nach onTerminate!
|
||||||
networkMonitor.stopMonitoring()
|
// 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
|
package dev.dettmer.simplenotes.sync
|
||||||
|
|
||||||
import android.content.Context
|
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.net.wifi.WifiManager
|
||||||
import android.os.Build
|
import androidx.work.*
|
||||||
import android.util.Log
|
|
||||||
import androidx.work.OneTimeWorkRequestBuilder
|
|
||||||
import androidx.work.OutOfQuotaPolicy
|
|
||||||
import androidx.work.WorkManager
|
|
||||||
import dev.dettmer.simplenotes.utils.Constants
|
import dev.dettmer.simplenotes.utils.Constants
|
||||||
|
import dev.dettmer.simplenotes.utils.Logger
|
||||||
import java.util.concurrent.TimeUnit
|
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) {
|
class NetworkMonitor(private val context: Context) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "NetworkMonitor"
|
private const val TAG = "NetworkMonitor"
|
||||||
|
private const val AUTO_SYNC_WORK_NAME = "auto_sync_periodic"
|
||||||
}
|
}
|
||||||
|
|
||||||
private val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE)
|
private val prefs by lazy {
|
||||||
as ConnectivityManager
|
context.getSharedPreferences(Constants.PREFS_NAME, Context.MODE_PRIVATE)
|
||||||
|
|
||||||
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")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Startet WorkManager mit Network Constraints
|
||||||
|
* WorkManager kümmert sich automatisch um WiFi-Erkennung!
|
||||||
|
*/
|
||||||
fun startMonitoring() {
|
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)
|
val autoSyncEnabled = prefs.getBoolean(Constants.KEY_AUTO_SYNC, false)
|
||||||
|
|
||||||
Log.d(TAG, "Auto-sync enabled: $autoSyncEnabled")
|
|
||||||
|
|
||||||
if (!autoSyncEnabled) {
|
if (!autoSyncEnabled) {
|
||||||
Log.d(TAG, "❌ Auto-sync disabled, skipping")
|
Logger.d(TAG, "Auto-sync disabled - stopping periodic work")
|
||||||
|
stopMonitoring()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val homeSSID = prefs.getString(Constants.KEY_HOME_SSID, null)
|
Logger.d(TAG, "🚀 Starting WorkManager-based auto-sync")
|
||||||
Log.d(TAG, "Home SSID configured: $homeSSID")
|
|
||||||
|
|
||||||
if (isConnectedToHomeWifi()) {
|
// Constraints: Nur wenn WiFi connected
|
||||||
Log.d(TAG, "✅ Connected to home WiFi, scheduling sync!")
|
val constraints = Constraints.Builder()
|
||||||
scheduleSyncWork()
|
.setRequiredNetworkType(NetworkType.UNMETERED) // WiFi only
|
||||||
} else {
|
.build()
|
||||||
Log.d(TAG, "❌ Not connected to home WiFi")
|
|
||||||
}
|
// 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)
|
* Stoppt WorkManager Auto-Sync
|
||||||
val homeSSID = prefs.getString(Constants.KEY_HOME_SSID, null) ?: return false
|
*/
|
||||||
|
fun stopMonitoring() {
|
||||||
val wifiManager = context.applicationContext.getSystemService(Context.WIFI_SERVICE)
|
Logger.d(TAG, "🛑 Stopping auto-sync")
|
||||||
as WifiManager
|
WorkManager.getInstance(context).cancelUniqueWork(AUTO_SYNC_WORK_NAME)
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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>()
|
val syncRequest = OneTimeWorkRequestBuilder<SyncWorker>()
|
||||||
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
|
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
|
||||||
@@ -162,7 +88,72 @@ class NetworkMonitor(private val context: Context) {
|
|||||||
.build()
|
.build()
|
||||||
|
|
||||||
WorkManager.getInstance(context).enqueue(syncRequest)
|
WorkManager.getInstance(context).enqueue(syncRequest)
|
||||||
|
}
|
||||||
|
|
||||||
Log.d(TAG, "✅ Sync work scheduled with WorkManager")
|
/**
|
||||||
|
* Prüft ob connected zu Home WiFi via Gateway IP Check
|
||||||
|
*/
|
||||||
|
private fun isConnectedToHomeWifi(): Boolean {
|
||||||
|
val gatewayIP = getGatewayIP() ?: return false
|
||||||
|
|
||||||
|
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
|
package dev.dettmer.simplenotes.sync
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.Log
|
import android.content.Intent
|
||||||
|
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||||
import androidx.work.CoroutineWorker
|
import androidx.work.CoroutineWorker
|
||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
|
import dev.dettmer.simplenotes.utils.Logger
|
||||||
import dev.dettmer.simplenotes.utils.NotificationHelper
|
import dev.dettmer.simplenotes.utils.NotificationHelper
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
@@ -15,44 +17,77 @@ class SyncWorker(
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "SyncWorker"
|
private const val TAG = "SyncWorker"
|
||||||
|
const val ACTION_SYNC_COMPLETED = "dev.dettmer.simplenotes.SYNC_COMPLETED"
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun doWork(): Result = withContext(Dispatchers.IO) {
|
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 {
|
return@withContext try {
|
||||||
// Show notification that sync is starting
|
// Start sync (kein "in progress" notification mehr)
|
||||||
NotificationHelper.showSyncInProgress(applicationContext)
|
|
||||||
Log.d(TAG, "📢 Notification shown: Sync in progress")
|
|
||||||
|
|
||||||
// Start sync
|
|
||||||
val syncService = WebDavSyncService(applicationContext)
|
val syncService = WebDavSyncService(applicationContext)
|
||||||
Log.d(TAG, "🚀 Starting sync...")
|
Logger.d(TAG, "🚀 Starting sync...")
|
||||||
|
Logger.d(TAG, "📊 Attempt: ${runAttemptCount}")
|
||||||
|
|
||||||
val result = syncService.syncNotes()
|
val result = syncService.syncNotes()
|
||||||
|
|
||||||
|
Logger.d(TAG, "📦 Sync result: success=${result.isSuccess}, count=${result.syncedCount}, error=${result.errorMessage}")
|
||||||
|
|
||||||
if (result.isSuccess) {
|
if (result.isSuccess) {
|
||||||
Log.d(TAG, "✅ Sync successful: ${result.syncedCount} notes")
|
Logger.d(TAG, "✅ Sync successful: ${result.syncedCount} notes")
|
||||||
NotificationHelper.showSyncSuccess(
|
|
||||||
applicationContext,
|
// Nur Notification zeigen wenn tatsächlich etwas gesynct wurde
|
||||||
result.syncedCount
|
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()
|
Result.success()
|
||||||
} else {
|
} else {
|
||||||
Log.e(TAG, "❌ Sync failed: ${result.errorMessage}")
|
Logger.e(TAG, "❌ Sync failed: ${result.errorMessage}")
|
||||||
NotificationHelper.showSyncError(
|
NotificationHelper.showSyncError(
|
||||||
applicationContext,
|
applicationContext,
|
||||||
result.errorMessage ?: "Unbekannter Fehler"
|
result.errorMessage ?: "Unbekannter Fehler"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Broadcast auch bei Fehler (damit UI refresht)
|
||||||
|
broadcastSyncCompleted(false, 0)
|
||||||
|
|
||||||
Result.failure()
|
Result.failure()
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} 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(
|
NotificationHelper.showSyncError(
|
||||||
applicationContext,
|
applicationContext,
|
||||||
e.message ?: "Unknown error"
|
e.message ?: "Unknown error"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
broadcastSyncCompleted(false, 0)
|
||||||
|
|
||||||
Result.failure()
|
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.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 kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
class WebDavSyncService(private val context: Context) {
|
class WebDavSyncService(private val context: Context) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "WebDavSyncService"
|
||||||
|
}
|
||||||
|
|
||||||
private val storage = NotesStorage(context)
|
private val storage = NotesStorage(context)
|
||||||
private val prefs = context.getSharedPreferences(Constants.PREFS_NAME, Context.MODE_PRIVATE)
|
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 username = prefs.getString(Constants.KEY_USERNAME, null) ?: return null
|
||||||
val password = prefs.getString(Constants.KEY_PASSWORD, 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 {
|
return OkHttpSardine().apply {
|
||||||
setCredentials(username, password)
|
setCredentials(username, password)
|
||||||
}
|
}
|
||||||
@@ -75,37 +83,59 @@ class WebDavSyncService(private val context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun syncNotes(): SyncResult = withContext(Dispatchers.IO) {
|
suspend fun syncNotes(): SyncResult = withContext(Dispatchers.IO) {
|
||||||
return@withContext try {
|
android.util.Log.d(TAG, "🔄 syncNotes() called")
|
||||||
val sardine = getSardine() ?: return@withContext SyncResult(
|
android.util.Log.d(TAG, "Context: ${context.javaClass.simpleName}")
|
||||||
isSuccess = false,
|
|
||||||
errorMessage = "Server-Zugangsdaten nicht konfiguriert"
|
|
||||||
)
|
|
||||||
|
|
||||||
val serverUrl = getServerUrl() ?: return@withContext SyncResult(
|
return@withContext try {
|
||||||
isSuccess = false,
|
val sardine = getSardine()
|
||||||
errorMessage = "Server-URL nicht konfiguriert"
|
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()
|
||||||
|
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 syncedCount = 0
|
||||||
var conflictCount = 0
|
var conflictCount = 0
|
||||||
|
|
||||||
// Ensure server directory exists
|
// Ensure server directory exists
|
||||||
|
android.util.Log.d(TAG, "🔍 Checking if server directory exists...")
|
||||||
if (!sardine.exists(serverUrl)) {
|
if (!sardine.exists(serverUrl)) {
|
||||||
|
android.util.Log.d(TAG, "📁 Creating server directory...")
|
||||||
sardine.createDirectory(serverUrl)
|
sardine.createDirectory(serverUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Upload local notes
|
// Upload local notes
|
||||||
|
android.util.Log.d(TAG, "⬆️ Uploading local notes...")
|
||||||
val uploadedCount = uploadLocalNotes(sardine, serverUrl)
|
val uploadedCount = uploadLocalNotes(sardine, serverUrl)
|
||||||
syncedCount += uploadedCount
|
syncedCount += uploadedCount
|
||||||
|
android.util.Log.d(TAG, "✅ Uploaded: $uploadedCount notes")
|
||||||
|
|
||||||
// Download remote notes
|
// Download remote notes
|
||||||
|
android.util.Log.d(TAG, "⬇️ Downloading remote notes...")
|
||||||
val downloadResult = downloadRemoteNotes(sardine, serverUrl)
|
val downloadResult = downloadRemoteNotes(sardine, serverUrl)
|
||||||
syncedCount += downloadResult.downloadedCount
|
syncedCount += downloadResult.downloadedCount
|
||||||
conflictCount += downloadResult.conflictCount
|
conflictCount += downloadResult.conflictCount
|
||||||
|
android.util.Log.d(TAG, "✅ Downloaded: ${downloadResult.downloadedCount} notes, Conflicts: ${downloadResult.conflictCount}")
|
||||||
|
|
||||||
// Update last sync timestamp
|
// Update last sync timestamp
|
||||||
saveLastSyncTimestamp()
|
saveLastSyncTimestamp()
|
||||||
|
|
||||||
|
android.util.Log.d(TAG, "🎉 Sync completed successfully - Total synced: $syncedCount")
|
||||||
|
|
||||||
SyncResult(
|
SyncResult(
|
||||||
isSuccess = true,
|
isSuccess = true,
|
||||||
syncedCount = syncedCount,
|
syncedCount = syncedCount,
|
||||||
@@ -113,11 +143,14 @@ class WebDavSyncService(private val context: Context) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
android.util.Log.e(TAG, "💥 Sync exception: ${e.message}", e)
|
||||||
|
android.util.Log.e(TAG, "Exception type: ${e.javaClass.name}")
|
||||||
|
|
||||||
SyncResult(
|
SyncResult(
|
||||||
isSuccess = false,
|
isSuccess = false,
|
||||||
errorMessage = when (e) {
|
errorMessage = when (e) {
|
||||||
is java.net.UnknownHostException -> "Server nicht erreichbar"
|
is java.net.UnknownHostException -> "Server nicht erreichbar: ${e.message}"
|
||||||
is java.net.SocketTimeoutException -> "Verbindungs-Timeout"
|
is java.net.SocketTimeoutException -> "Verbindungs-Timeout: ${e.message}"
|
||||||
is javax.net.ssl.SSLException -> "SSL-Fehler"
|
is javax.net.ssl.SSLException -> "SSL-Fehler"
|
||||||
is com.thegrizzlylabs.sardineandroid.impl.SardineException -> {
|
is com.thegrizzlylabs.sardineandroid.impl.SardineException -> {
|
||||||
when (e.statusCode) {
|
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
|
* Zeigt Erfolgs-Notification
|
||||||
*/
|
*/
|
||||||
fun showSyncSuccess(context: Context, count: Int) {
|
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)
|
val notification = NotificationCompat.Builder(context, CHANNEL_ID)
|
||||||
.setSmallIcon(android.R.drawable.stat_notify_sync)
|
.setSmallIcon(android.R.drawable.stat_notify_sync)
|
||||||
.setContentTitle("Sync erfolgreich")
|
.setContentTitle("Sync erfolgreich")
|
||||||
.setContentText("$count Notizen synchronisiert")
|
.setContentText("$count Notizen synchronisiert")
|
||||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||||
.setAutoCancel(true)
|
.setCategory(NotificationCompat.CATEGORY_STATUS)
|
||||||
|
.setContentIntent(pendingIntent) // Click öffnet App
|
||||||
|
.setAutoCancel(true) // Dismiss beim Click
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val manager = context.getSystemService(Context.NOTIFICATION_SERVICE)
|
val manager = context.getSystemService(Context.NOTIFICATION_SERVICE)
|
||||||
@@ -229,12 +242,25 @@ object NotificationHelper {
|
|||||||
* Zeigt Fehler-Notification
|
* Zeigt Fehler-Notification
|
||||||
*/
|
*/
|
||||||
fun showSyncError(context: Context, message: String) {
|
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)
|
val notification = NotificationCompat.Builder(context, CHANNEL_ID)
|
||||||
.setSmallIcon(android.R.drawable.stat_notify_error)
|
.setSmallIcon(android.R.drawable.stat_notify_error)
|
||||||
.setContentTitle("Sync Fehler")
|
.setContentTitle("Sync Fehler")
|
||||||
.setContentText(message)
|
.setContentText(message)
|
||||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||||
.setAutoCancel(true)
|
.setCategory(NotificationCompat.CATEGORY_ERROR)
|
||||||
|
.setContentIntent(pendingIntent) // Click öffnet App
|
||||||
|
.setAutoCancel(true) // Dismiss beim Click
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val manager = context.getSystemService(Context.NOTIFICATION_SERVICE)
|
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_marginTop="16dp"
|
||||||
android:layout_marginBottom="16dp" />
|
android:layout_marginBottom="16dp" />
|
||||||
|
|
||||||
|
<!-- Server Status -->
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
android:layout_marginBottom="8dp">
|
android:gravity="center_vertical">
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputLayout
|
<TextView
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:hint="@string/home_ssid"
|
android:text="Server-Status:"
|
||||||
android:layout_marginEnd="8dp"
|
android:textStyle="bold"
|
||||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox">
|
android:textSize="14sp" />
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputEditText
|
<TextView
|
||||||
android:id="@+id/editTextHomeSSID"
|
android:id="@+id/textViewServerStatus"
|
||||||
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:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center_vertical"
|
android:text="Prüfe..."
|
||||||
android:text="Detect"
|
android:textSize="14sp"
|
||||||
style="@style/Widget.Material3.Button.OutlinedButton" />
|
android:textColor="@android:color/darker_gray" />
|
||||||
|
|
||||||
</LinearLayout>
|
</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
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
|||||||
Reference in New Issue
Block a user