feat: Konfigurierbare Sync-Intervalle + Über-Sektion (v1.1.0) (#1)

* feat: WiFi-Connect Auto-Sync + Debug Logging [skip ci]

- WiFi-Connect Auto-Sync via NetworkCallback + Broadcast (statt WorkManager)
- onResume Auto-Sync mit Toast-Feedback (nur Success)
- File-Logging Feature für Debugging (letzte 500 Einträge)
- Settings: Debug/Logs Section mit Test-Button
- FileProvider für Log-Sharing
- Extensive Debug-Logs für NetworkMonitor + MainActivity
- Material Design 3 Migration (alle 17 Tasks)
- Bug-Fixes: Input underlines, section rename, swipe-to-delete, flat cards

PROBLEM: WiFi-Connect sendet Broadcast aber MainActivity empfängt nicht
→ Benötigt logcat debugging auf anderem Gerät

* 🐛 fix: Remove WiFi-Connect related code and UI elements to streamline sync process

* feat: Konfigurierbare Sync-Intervalle + Über-Sektion (v1.1.0)

## Neue Features

### Konfigurierbare Sync-Intervalle
- Wählbare Intervalle: 15/30/60 Minuten in Settings
- Transparente Akkuverbrauchs-Anzeige (0.2-0.8% pro Tag)
- Sofortige Anwendung ohne App-Neustart
- NetworkMonitor liest Intervall dynamisch aus SharedPreferences

### Über-Sektion
- App-Version & Build-Datum Anzeige
- Klickbare Links zu GitHub Repository & Entwickler-Profil
- Lizenz-Information (MIT License)
- Ersetzt alte Debug/Logs Sektion

## Verbesserungen

- Benutzerfreundliche Doze-Mode Erklärung in Settings
- Keine störenden Sync-Fehler Toasts mehr im Hintergrund
- Modernisierte README mit Badges und kompakter Struktur
- F-Droid Metadaten aktualisiert (changelogs + descriptions)

## Technische Änderungen

- Version Bump: 1.0 → 1.1.0 (versionCode: 1 → 2)
- BUILD_DATE buildConfigField hinzugefügt
- PREF_SYNC_INTERVAL_MINUTES Konstante in Constants.kt
- NetworkMonitor.startPeriodicSync() nutzt konfigurierbare Intervalle
- SettingsActivity: setupSyncIntervalPicker() + setupAboutSection()
- activity_settings.xml: RadioGroup für Intervalle + About Cards
This commit is contained in:
Inventory69
2025-12-22 00:49:24 +01:00
committed by GitHub
parent 86c5e62fd6
commit c55b64dab3
33 changed files with 4687 additions and 466 deletions

View File

@@ -10,25 +10,43 @@ import android.util.Log
import android.view.MenuItem
import android.widget.Button
import android.widget.EditText
import android.widget.RadioGroup
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.SwitchCompat
import androidx.core.content.FileProvider
import androidx.lifecycle.lifecycleScope
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import com.google.android.material.appbar.MaterialToolbar
import dev.dettmer.simplenotes.sync.WebDavSyncService
import dev.dettmer.simplenotes.utils.Constants
import dev.dettmer.simplenotes.utils.showToast
import com.google.android.material.card.MaterialCardView
import com.google.android.material.chip.Chip
import com.google.android.material.color.DynamicColors
import com.google.android.material.switchmaterial.SwitchMaterial
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import dev.dettmer.simplenotes.sync.WebDavSyncService
import dev.dettmer.simplenotes.sync.NetworkMonitor
import dev.dettmer.simplenotes.utils.Constants
import dev.dettmer.simplenotes.utils.Logger
import dev.dettmer.simplenotes.utils.showToast
import java.io.File
import java.net.HttpURLConnection
import java.net.URL
import java.text.SimpleDateFormat
import java.util.Locale
class SettingsActivity : AppCompatActivity() {
companion object {
private const val TAG = "SettingsActivity"
private const val GITHUB_REPO_URL = "https://github.com/inventory69/simple-notes-sync"
private const val GITHUB_PROFILE_URL = "https://github.com/inventory69"
private const val LICENSE_URL = "https://github.com/inventory69/simple-notes-sync/blob/main/LICENSE"
}
private lateinit var editTextServerUrl: EditText
@@ -37,7 +55,20 @@ class SettingsActivity : AppCompatActivity() {
private lateinit var switchAutoSync: SwitchCompat
private lateinit var buttonTestConnection: Button
private lateinit var buttonSyncNow: Button
private lateinit var buttonRestoreFromServer: Button
private lateinit var textViewServerStatus: TextView
private lateinit var chipAutoSaveStatus: Chip
// Sync Interval UI
private lateinit var radioGroupSyncInterval: RadioGroup
// About Section UI
private lateinit var textViewAppVersion: TextView
private lateinit var cardGitHubRepo: MaterialCardView
private lateinit var cardDeveloperProfile: MaterialCardView
private lateinit var cardLicense: MaterialCardView
private var autoSaveIndicatorJob: Job? = null
private val prefs by lazy {
getSharedPreferences(Constants.PREFS_NAME, MODE_PRIVATE)
@@ -45,6 +76,10 @@ class SettingsActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Apply Dynamic Colors for Android 12+ (Material You)
DynamicColors.applyToActivityIfAvailable(this)
setContentView(R.layout.activity_settings)
// Setup toolbar
@@ -58,6 +93,8 @@ class SettingsActivity : AppCompatActivity() {
findViews()
loadSettings()
setupListeners()
setupSyncIntervalPicker()
setupAboutSection()
}
private fun findViews() {
@@ -67,7 +104,18 @@ class SettingsActivity : AppCompatActivity() {
switchAutoSync = findViewById(R.id.switchAutoSync)
buttonTestConnection = findViewById(R.id.buttonTestConnection)
buttonSyncNow = findViewById(R.id.buttonSyncNow)
buttonRestoreFromServer = findViewById(R.id.buttonRestoreFromServer)
textViewServerStatus = findViewById(R.id.textViewServerStatus)
chipAutoSaveStatus = findViewById(R.id.chipAutoSaveStatus)
// Sync Interval UI
radioGroupSyncInterval = findViewById(R.id.radioGroupSyncInterval)
// About Section UI
textViewAppVersion = findViewById(R.id.textViewAppVersion)
cardGitHubRepo = findViewById(R.id.cardGitHubRepo)
cardDeveloperProfile = findViewById(R.id.cardDeveloperProfile)
cardLicense = findViewById(R.id.cardLicense)
}
private fun loadSettings() {
@@ -91,16 +139,122 @@ class SettingsActivity : AppCompatActivity() {
syncNow()
}
buttonRestoreFromServer.setOnClickListener {
saveSettings()
showRestoreConfirmation()
}
switchAutoSync.setOnCheckedChangeListener { _, isChecked ->
onAutoSyncToggled(isChecked)
showAutoSaveIndicator()
}
// Server Status Check bei Settings-Änderung
editTextServerUrl.setOnFocusChangeListener { _, hasFocus ->
if (!hasFocus) {
checkServerStatus()
showAutoSaveIndicator()
}
}
editTextUsername.setOnFocusChangeListener { _, hasFocus ->
if (!hasFocus) showAutoSaveIndicator()
}
editTextPassword.setOnFocusChangeListener { _, hasFocus ->
if (!hasFocus) showAutoSaveIndicator()
}
}
/**
* Setup sync interval picker with radio buttons
*/
private fun setupSyncIntervalPicker() {
// Load current interval from preferences
val currentInterval = prefs.getLong(Constants.PREF_SYNC_INTERVAL_MINUTES, Constants.DEFAULT_SYNC_INTERVAL_MINUTES)
// Set checked radio button based on current interval
val checkedId = when (currentInterval) {
15L -> R.id.radioInterval15
30L -> R.id.radioInterval30
60L -> R.id.radioInterval60
else -> R.id.radioInterval30 // Default
}
radioGroupSyncInterval.check(checkedId)
// Listen for interval changes
radioGroupSyncInterval.setOnCheckedChangeListener { _, checkedId ->
val newInterval = when (checkedId) {
R.id.radioInterval15 -> 15L
R.id.radioInterval60 -> 60L
else -> 30L // R.id.radioInterval30 or fallback
}
// Save new interval to preferences
prefs.edit().putLong(Constants.PREF_SYNC_INTERVAL_MINUTES, newInterval).apply()
// Restart periodic sync with new interval (only if auto-sync is enabled)
if (prefs.getBoolean(Constants.KEY_AUTO_SYNC, false)) {
val networkMonitor = NetworkMonitor(this)
networkMonitor.startMonitoring()
val intervalText = when (newInterval) {
15L -> "15 Minuten"
30L -> "30 Minuten"
60L -> "60 Minuten"
else -> "$newInterval Minuten"
}
showToast("⏱️ Sync-Intervall auf $intervalText geändert")
Logger.i(TAG, "Sync interval changed to $newInterval minutes, restarted periodic sync")
} else {
showToast("⏱️ Sync-Intervall gespeichert (Auto-Sync ist deaktiviert)")
}
}
}
/**
* Setup about section with version info and clickable cards
*/
private fun setupAboutSection() {
// Display app version with build date
try {
val versionName = BuildConfig.VERSION_NAME
val versionCode = BuildConfig.VERSION_CODE
val buildDate = BuildConfig.BUILD_DATE
textViewAppVersion.text = "Version $versionName ($versionCode)\nErstellt am: $buildDate"
} catch (e: Exception) {
Logger.e(TAG, "Failed to load version info", e)
textViewAppVersion.text = "Version nicht verfügbar"
}
// GitHub Repository Card
cardGitHubRepo.setOnClickListener {
openUrl(GITHUB_REPO_URL)
}
// Developer Profile Card
cardDeveloperProfile.setOnClickListener {
openUrl(GITHUB_PROFILE_URL)
}
// License Card
cardLicense.setOnClickListener {
openUrl(LICENSE_URL)
}
}
/**
* Opens URL in browser
*/
private fun openUrl(url: String) {
try {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
startActivity(intent)
} catch (e: Exception) {
Logger.e(TAG, "Failed to open URL: $url", e)
showToast("❌ Fehler beim Öffnen des Links")
}
}
private fun saveSettings() {
@@ -122,11 +276,14 @@ class SettingsActivity : AppCompatActivity() {
if (result.isSuccess) {
showToast("Verbindung erfolgreich!")
checkServerStatus() // ✅ Server-Status sofort aktualisieren
} else {
showToast("Verbindung fehlgeschlagen: ${result.errorMessage}")
checkServerStatus() // ✅ Auch bei Fehler aktualisieren
}
} catch (e: Exception) {
showToast("Fehler: ${e.message}")
checkServerStatus() // ✅ Auch bei Exception aktualisieren
}
}
}
@@ -144,11 +301,14 @@ class SettingsActivity : AppCompatActivity() {
} else {
showToast("Erfolgreich! ${result.syncedCount} Notizen synchronisiert")
}
checkServerStatus() // ✅ Server-Status nach Sync aktualisieren
} else {
showToast("Sync fehlgeschlagen: ${result.errorMessage}")
checkServerStatus() // ✅ Auch bei Fehler aktualisieren
}
} catch (e: Exception) {
showToast("Fehler: ${e.message}")
checkServerStatus() // ✅ Auch bei Exception aktualisieren
}
}
}
@@ -260,6 +420,75 @@ class SettingsActivity : AppCompatActivity() {
}
}
private fun showAutoSaveIndicator() {
// Cancel previous job if still running
autoSaveIndicatorJob?.cancel()
// Show saving indicator
chipAutoSaveStatus.apply {
visibility = android.view.View.VISIBLE
text = "💾 Speichere..."
setChipBackgroundColorResource(android.R.color.darker_gray)
}
// Save settings
saveSettings()
// Show saved confirmation after short delay
autoSaveIndicatorJob = lifecycleScope.launch {
delay(300) // Short delay to show "Speichere..."
chipAutoSaveStatus.apply {
text = "✓ Gespeichert"
setChipBackgroundColorResource(android.R.color.holo_green_light)
}
delay(2000) // Show for 2 seconds
chipAutoSaveStatus.visibility = android.view.View.GONE
}
}
private fun showRestoreConfirmation() {
android.app.AlertDialog.Builder(this)
.setTitle(R.string.restore_confirmation_title)
.setMessage(R.string.restore_confirmation_message)
.setPositiveButton(R.string.restore_button) { _, _ ->
performRestore()
}
.setNegativeButton(R.string.cancel, null)
.show()
}
private fun performRestore() {
val progressDialog = android.app.ProgressDialog(this).apply {
setMessage(getString(R.string.restore_progress))
setCancelable(false)
show()
}
CoroutineScope(Dispatchers.Main).launch {
try {
val webdavService = WebDavSyncService(this@SettingsActivity)
val result = withContext(Dispatchers.IO) {
webdavService.restoreFromServer()
}
progressDialog.dismiss()
if (result.isSuccess) {
showToast(getString(R.string.restore_success, result.restoredCount))
// Refresh MainActivity's note list
setResult(RESULT_OK)
} else {
showToast(getString(R.string.restore_error, result.errorMessage))
}
checkServerStatus()
} catch (e: Exception) {
progressDialog.dismiss()
showToast(getString(R.string.restore_error, e.message))
checkServerStatus()
}
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
android.R.id.home -> {