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:
@@ -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 -> {
|
||||
|
||||
Reference in New Issue
Block a user