🚀 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:
inventory69
2025-12-21 11:09:29 +01:00
parent 933646f28b
commit 7e277e7fb9
19 changed files with 1866 additions and 562 deletions

View File

@@ -1,50 +1,48 @@
package dev.dettmer.simplenotes
import android.Manifest
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.net.wifi.WifiManager
import android.os.Build
import android.os.Bundle
import android.os.PowerManager
import android.provider.Settings
import android.util.Log
import android.view.MenuItem
import android.widget.Button
import android.widget.EditText
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.SwitchCompat
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope
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 kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.net.HttpURLConnection
import java.net.URL
class SettingsActivity : AppCompatActivity() {
companion object {
private const val TAG = "SettingsActivity"
}
private lateinit var editTextServerUrl: EditText
private lateinit var editTextUsername: EditText
private lateinit var editTextPassword: EditText
private lateinit var editTextHomeSSID: EditText
private lateinit var switchAutoSync: SwitchCompat
private lateinit var buttonTestConnection: Button
private lateinit var buttonSyncNow: Button
private lateinit var buttonDetectSSID: Button
private lateinit var textViewServerStatus: TextView
private val prefs by lazy {
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?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_settings)
@@ -66,19 +64,20 @@ class SettingsActivity : AppCompatActivity() {
editTextServerUrl = findViewById(R.id.editTextServerUrl)
editTextUsername = findViewById(R.id.editTextUsername)
editTextPassword = findViewById(R.id.editTextPassword)
editTextHomeSSID = findViewById(R.id.editTextHomeSSID)
switchAutoSync = findViewById(R.id.switchAutoSync)
buttonTestConnection = findViewById(R.id.buttonTestConnection)
buttonSyncNow = findViewById(R.id.buttonSyncNow)
buttonDetectSSID = findViewById(R.id.buttonDetectSSID)
textViewServerStatus = findViewById(R.id.textViewServerStatus)
}
private fun loadSettings() {
editTextServerUrl.setText(prefs.getString(Constants.KEY_SERVER_URL, ""))
editTextUsername.setText(prefs.getString(Constants.KEY_USERNAME, ""))
editTextPassword.setText(prefs.getString(Constants.KEY_PASSWORD, ""))
editTextHomeSSID.setText(prefs.getString(Constants.KEY_HOME_SSID, ""))
switchAutoSync.isChecked = prefs.getBoolean(Constants.KEY_AUTO_SYNC, false)
// Server Status prüfen
checkServerStatus()
}
private fun setupListeners() {
@@ -92,13 +91,16 @@ class SettingsActivity : AppCompatActivity() {
syncNow()
}
buttonDetectSSID.setOnClickListener {
detectCurrentSSID()
}
switchAutoSync.setOnCheckedChangeListener { _, isChecked ->
onAutoSyncToggled(isChecked)
}
// Server Status Check bei Settings-Änderung
editTextServerUrl.setOnFocusChangeListener { _, hasFocus ->
if (!hasFocus) {
checkServerStatus()
}
}
}
private fun saveSettings() {
@@ -106,7 +108,6 @@ class SettingsActivity : AppCompatActivity() {
putString(Constants.KEY_SERVER_URL, editTextServerUrl.text.toString().trim())
putString(Constants.KEY_USERNAME, editTextUsername.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)
apply()
}
@@ -152,34 +153,41 @@ class SettingsActivity : AppCompatActivity() {
}
}
private fun detectCurrentSSID() {
// Check if we have location permission (needed for SSID on Android 10+)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
if (ContextCompat.checkSelfPermission(
this,
Manifest.permission.ACCESS_FINE_LOCATION
) != PackageManager.PERMISSION_GRANTED
) {
// Request permission
ActivityCompat.requestPermissions(
this,
arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
REQUEST_LOCATION_PERMISSION
)
return
}
private fun checkServerStatus() {
val serverUrl = prefs.getString(Constants.KEY_SERVER_URL, null)
if (serverUrl.isNullOrEmpty()) {
textViewServerStatus.text = "❌ Nicht konfiguriert"
textViewServerStatus.setTextColor(getColor(android.R.color.holo_red_dark))
return
}
// Permission granted, get SSID
val wifiManager = applicationContext.getSystemService(WIFI_SERVICE) as WifiManager
val wifiInfo = wifiManager.connectionInfo
val ssid = wifiInfo.ssid.replace("\"", "")
textViewServerStatus.text = "🔍 Prüfe..."
textViewServerStatus.setTextColor(getColor(android.R.color.darker_gray))
if (ssid.isNotEmpty() && ssid != "<unknown ssid>") {
editTextHomeSSID.setText(ssid)
showToast("SSID erkannt: $ssid")
} else {
showToast("Nicht mit WLAN verbunden")
lifecycleScope.launch {
val isReachable = withContext(Dispatchers.IO) {
try {
val url = URL(serverUrl)
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) {
showToast("Auto-Sync aktiviert")
// Check battery optimization when enabling
checkBatteryOptimization()
// Check background location permission (needed for SSID on Android 12+)
checkBackgroundLocationPermission()
restartNetworkMonitor()
} else {
showToast("Auto-Sync deaktiviert")
restartNetworkMonitor()
}
}
@@ -240,99 +247,16 @@ class SettingsActivity : AppCompatActivity() {
}
}
private fun checkBackgroundLocationPermission() {
// Background location permission only needed on Android 10+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
return
}
// First check if we have foreground location
if (ContextCompat.checkSelfPermission(
this,
Manifest.permission.ACCESS_FINE_LOCATION
) != 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")
}
}
private fun restartNetworkMonitor() {
try {
val app = application as SimpleNotesApplication
Log.d(TAG, "🔄 Restarting NetworkMonitor with new settings")
app.networkMonitor.stopMonitoring()
app.networkMonitor.startMonitoring()
Log.d(TAG, "✅ NetworkMonitor restarted successfully")
} catch (e: Exception) {
Log.e(TAG, "❌ Failed to restart NetworkMonitor", e)
showToast("Fehler beim Neustart des NetworkMonitors")
}
}