added dummy accounts, change passwort is now a feature, toast when session takes to long, online frontend fix

This commit is contained in:
oxidiert
2025-10-16 13:19:54 +02:00
parent 39a4811fd2
commit 5b1264293c
4 changed files with 351 additions and 83 deletions

View File

@ -25,19 +25,6 @@ object DatabaseUploader {
private val client = OkHttpClient()
fun uploadDatabaseWithLogin(context: Context, username: String, password: String) {
LoginManager.loginUserWithCredentials(
context = context,
username = username,
password = password,
onSuccess = { token ->
Log.d("UPLOAD", "Login OK (user=$username)")
uploadDatabase(context, token)
},
onError = { msg -> Log.e("UPLOAD", "Login fehlgeschlagen: $msg") }
)
}
private fun uploadDatabase(context: Context, token: String) {
CoroutineScope(Dispatchers.IO).launch {
try {
@ -171,6 +158,8 @@ object DatabaseUploader {
}
fun uploadDatabaseWithToken(context: Context, token: String) {
uploadDatabase(context, token) // nutzt die bestehende interne Logik
uploadDatabase(context, token)
}
}

View File

@ -13,6 +13,8 @@ import org.json.JSONArray
import org.json.JSONObject
import java.io.File
import java.util.concurrent.TimeUnit
import com.google.android.material.dialog.MaterialAlertDialogBuilder
var RHS_POINTS: Int? = null
@ -32,6 +34,8 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
private lateinit var databaseButton: Button
private lateinit var statusSession: TextView
private lateinit var statusOnline: TextView
private val SESSION_WARN_AFTER_MS = 12 * 60 * 60 * 1000L // 12h
private var sessionLongWarnedOnce = false
private val dynamicButtons = mutableListOf<Button>()
private val questionnaireFiles = mutableMapOf<Button, String>()
@ -63,6 +67,11 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
fun init() {
activity.setContentView(R.layout.opening_screen)
// <<< NEU: bei jedem Öffnen des Screens zurücksetzen,
// damit der Toast pro Besuch einmal angezeigt wird
sessionLongWarnedOnce = false
bindViews()
loadQuestionnaireOrder()
createQuestionnaireButtons()
@ -82,9 +91,14 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
val pathExists = File("/data/data/com.dano.test1/databases/questionnaire_database").exists()
updateMainButtonsState(pathExists)
updateDownloadButtonState(pathExists) // <<< NEU: Download-Button anhand DB-Status setzen
updateDownloadButtonState(pathExists)
if (pathExists && !editText.text.isNullOrBlank()) buttonLoad.performClick()
uiHandler.removeCallbacks(statusTicker)
updateStatusStrip()
applySessionAgeHighlight(System.currentTimeMillis() - TokenStore.getLoginTimestamp(activity))
uiHandler.post(statusTicker)
}
private fun bindViews() {
@ -97,7 +111,10 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
saveButton = activity.findViewById(R.id.saveButton)
editButton = activity.findViewById(R.id.editButton)
uploadButton = activity.findViewById(R.id.uploadButton)
downloadButton = activity.findViewById(R.id.downloadButton)
downloadButton.visibility = View.GONE
databaseButton = activity.findViewById(R.id.databaseButton)
statusSession = activity.findViewById(R.id.statusSession)
statusOnline = activity.findViewById(R.id.statusOnline)
@ -444,53 +461,50 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
private fun setupUploadButton() {
uploadButton.text = t("upload")
uploadButton.setOnClickListener {
// 1) Token-Frische prüfen (24h Gültigkeit; wir nehmen 23h als Puffer)
val existingToken = TokenStore.getToken(activity)
val ageMs = System.currentTimeMillis() - TokenStore.getLoginTimestamp(activity)
val isFresh = !existingToken.isNullOrBlank() && ageMs < 23 * 60 * 60 * 1000
// ZUERST bestätigen lassen
confirmUpload {
// === dein bestehender Upload-Code unverändert ===
val existingToken = TokenStore.getToken(activity)
val ageMs = System.currentTimeMillis() - TokenStore.getLoginTimestamp(activity)
val isFresh = !existingToken.isNullOrBlank() && ageMs < 23 * 60 * 60 * 1000
if (isFresh) {
// Frischer Token -> direkt hochladen
GlobalValues.LAST_CLIENT_CODE = editText.text.toString().trim()
DatabaseUploader.uploadDatabaseWithToken(activity, existingToken!!)
return@setOnClickListener
}
// 2) Kein/alter Token -> Login anstoßen
val username = TokenStore.getUsername(activity)?.trim().orEmpty()
if (username.isBlank()) {
Toast.makeText(activity, t("login_required"), Toast.LENGTH_LONG).show()
return@setOnClickListener
}
// *** NUR für deine Testumgebung: bekannte Passwörter ableiten ***
// user01 -> pw1, user02 -> pw2 (entspricht login.php)
val password = when (username) {
"user01" -> "pw1"
"user02" -> "pw2"
else -> {
Toast.makeText(activity, t("login_required"), Toast.LENGTH_LONG).show()
return@setOnClickListener
}
}
// Re-Login -> neuen Token speichern -> Upload starten
LoginManager.loginUserWithCredentials(
context = activity,
username = username,
password = password,
onSuccess = { freshToken ->
if (isFresh) {
GlobalValues.LAST_CLIENT_CODE = editText.text.toString().trim()
DatabaseUploader.uploadDatabaseWithToken(activity, freshToken)
},
onError = { msg ->
Toast.makeText(activity, t("login_failed_with_reason").replace("{reason}", msg), Toast.LENGTH_LONG).show()
DatabaseUploader.uploadDatabaseWithToken(activity, existingToken!!)
return@confirmUpload
}
)
val username = TokenStore.getUsername(activity)?.trim().orEmpty()
if (username.isBlank()) {
Toast.makeText(activity, t("login_required"), Toast.LENGTH_LONG).show()
return@confirmUpload
}
val password = when (username) {
"user01" -> "pw1"
"user02" -> "pw2"
else -> {
Toast.makeText(activity, t("login_required"), Toast.LENGTH_LONG).show()
return@confirmUpload
}
}
LoginManager.loginUserWithCredentials(
context = activity,
username = username,
password = password,
onSuccess = { freshToken ->
GlobalValues.LAST_CLIENT_CODE = editText.text.toString().trim()
DatabaseUploader.uploadDatabaseWithToken(activity, freshToken)
},
onError = { msg ->
Toast.makeText(activity, t("login_failed_with_reason").replace("{reason}", msg), Toast.LENGTH_LONG).show()
}
)
}
}
}
private fun setupDownloadButton() {
downloadButton.text = t("download")
@ -522,7 +536,6 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
// Der Download-Button wird separat gesteuert
}
/** <<< NEU: Steuert Aktivierung & Look des Download-Buttons je nach DB-Verfügbarkeit */
private fun updateDownloadButtonState(isDatabaseAvailable: Boolean) {
val mb = downloadButton as? MaterialButton
if (isDatabaseAvailable) {
@ -619,13 +632,18 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
val h = TimeUnit.MILLISECONDS.toHours(ageMs)
val m = TimeUnit.MILLISECONDS.toMinutes(ageMs) - h * 60
if (ts > 0L) {
statusSession.text = "${t("session_label")}: ${h}${t("hours_short")} ${m}${t("minutes_short")}"
// ⚠️ anhängen, wenn >12h, der eigentliche Hinweis/Styling kommt aus applySessionAgeHighlight()
val warn = if (ageMs >= SESSION_WARN_AFTER_MS) " ⚠️" else ""
statusSession.text = "${t("session_label")}: ${h}${t("hours_short")} ${m}${t("minutes_short")}$warn"
} else {
statusSession.text = t("session_dash")
}
val online = NetworkUtils.isOnline(activity)
statusOnline.text = if (online) t("online") else t("offline")
statusOnline.setTextColor(if (online) Color.parseColor("#2E7D32") else Color.parseColor("#C62828"))
// <<< NEU: hier jeweils prüfen/markieren
applySessionAgeHighlight(ageMs)
}
fun refreshHeaderStatusLive() {
@ -644,4 +662,58 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
coachEditText.compoundDrawablePadding = dp(8)
coachEditText.alpha = 0.95f
}
private fun applySessionAgeHighlight(ageMs: Long) {
val isOld = ageMs >= SESSION_WARN_AFTER_MS
if (isOld) {
statusSession.setTextColor(Color.parseColor("#C62828"))
statusSession.setBackgroundColor(Color.parseColor("#FFF3CD"))
statusSession.setPadding(dp(8), dp(4), dp(8), dp(4))
if (!sessionLongWarnedOnce) {
showRedToast(activity, t("session_over_12"))
sessionLongWarnedOnce = true
}
} else {
statusSession.setTextColor(Color.parseColor("#2F2A49"))
statusSession.setBackgroundColor(Color.TRANSPARENT)
statusSession.setPadding(0, 0, 0, 0)
}
}
private fun showRedToast(ctx: android.content.Context, message: String) {
val tv = android.widget.TextView(ctx).apply {
text = message
setTextColor(android.graphics.Color.WHITE)
textSize = 16f
setPadding(32, 20, 32, 20)
background = android.graphics.drawable.GradientDrawable().apply {
shape = android.graphics.drawable.GradientDrawable.RECTANGLE
cornerRadius = 24f
setColor(android.graphics.Color.parseColor("#D32F2F")) // kräftiges Rot
}
}
android.widget.Toast(ctx).apply {
duration = android.widget.Toast.LENGTH_LONG
view = tv
setGravity(android.view.Gravity.TOP or android.view.Gravity.CENTER_HORIZONTAL, 0, 120)
}.show()
}
private fun confirmUpload(onConfirm: () -> Unit) {
MaterialAlertDialogBuilder(activity)
.setTitle(t("start_upload"))
.setMessage(t("ask_before_upload"))
.setPositiveButton(t("ok")) { d, _ ->
d.dismiss()
onConfirm()
}
.setNegativeButton(t("cancel")) { d, _ ->
d.dismiss()
}
.show()
}
}

View File

@ -400,7 +400,12 @@ object LanguageManager {
"done" to "Erledigt",
"not_done" to "Nicht erledigt",
"none" to "Keine",
"view_missing" to "Fehlende View: %s"
"view_missing" to "Fehlende View: %s",
"session_over_12" to "Sitzung läuft länger als 12 Stunden.",
"cancel" to "Cancel",
"ok" to "OK",
"ask_before_upload" to "Möchtest du den Upload wirklich ausführen?",
"start_upload" to "Upload starten?"
),
"ENGLISH" to mapOf(
@ -771,7 +776,12 @@ object LanguageManager {
"done" to "Done",
"not_done" to "Not done",
"none" to "None",
"view_missing" to "Missing view: %s"
"view_missing" to "Missing view: %s",
"session_over_12" to "Session has been running for more than 12 hours.",
"cancel" to "Cancel",
"ok" to "OK",
"ask_before_upload" to "Do you really want to perform the upload?",
"start_upload" to "Start upload?"
),
"FRENCH" to mapOf(
@ -1146,7 +1156,12 @@ object LanguageManager {
"done" to "Terminé",
"not_done" to "Non terminé",
"none" to "Aucun",
"view_missing" to "Vue manquante : %s"
"view_missing" to "Vue manquante : %s",
"session_over_12" to "La session dure depuis plus de 12 heures.",
"cancel" to "Annuler",
"ok" to "OK",
"ask_before_upload" to "Voulez-vous vraiment effectuer le téléversement ?",
"start_upload" to "Démarrer le téléversement ?"
),
"RUSSIAN" to mapOf(
@ -1517,7 +1532,12 @@ object LanguageManager {
"done" to "Готово",
"not_done" to "Не выполнено",
"none" to "Нет",
"view_missing" to "Отсутствует представление: %s"
"view_missing" to "Отсутствует представление: %s",
"session_over_12" to "Сеанс продолжается более 12 часов.",
"cancel" to "Отмена",
"ok" to "OK",
"ask_before_upload" to "Вы действительно хотите выполнить загрузку?",
"start_upload" to "Начать загрузку?"
),
"UKRAINIAN" to mapOf(
@ -1892,7 +1912,12 @@ object LanguageManager {
"done" to "Готово",
"not_done" to "Не виконано",
"none" to "Немає",
"view_missing" to "Відсутній елемент інтерфейсу: %s"
"view_missing" to "Відсутній елемент інтерфейсу: %s",
"session_over_12" to "Сеанс триває понад 12 годин.",
"cancel" to "Скасувати",
"ok" to "OK",
"ask_before_upload" to "Ви справді хочете виконати завантаження?",
"start_upload" to "Почати завантаження?"
),
"TURKISH" to mapOf(
@ -2267,7 +2292,12 @@ object LanguageManager {
"done" to "Tamamlandı",
"not_done" to "Tamamlanmadı",
"none" to "Yok",
"view_missing" to "Eksik görünüm: %s"
"view_missing" to "Eksik görünüm: %s",
"session_over_12" to "Oturum 12 saatten uzun süredir açık.",
"cancel" to "İptal",
"ok" to "Tamam",
"ask_before_upload" to "Yüklemeyi gerçekten yapmak istiyor musunuz?",
"start_upload" to "Yüklemeyi başlat?"
),
"POLISH" to mapOf(
@ -2642,7 +2672,12 @@ object LanguageManager {
"done" to "Zrobione",
"not_done" to "Niezrobione",
"none" to "Brak",
"view_missing" to "Brak widoku: %s"
"view_missing" to "Brak widoku: %s",
"session_over_12" to "Sesja trwa dłużej niż 12 godzin.",
"cancel" to "Anuluj",
"ok" to "OK",
"ask_before_upload" to "Czy na pewno chcesz wykonać przesyłanie?",
"start_upload" to "Rozpocząć przesyłanie?"
),
"ARABIC" to mapOf(
@ -3017,7 +3052,12 @@ object LanguageManager {
"done" to "منجز",
"not_done" to "غير منجز",
"none" to "لا شيء",
"view_missing" to "العنصر المفقود: %s"
"view_missing" to "العنصر المفقود: %s",
"session_over_12" to "تعمل الجلسة منذ أكثر من 12 ساعة.",
"cancel" to "إلغاء",
"ok" to "موافق",
"ask_before_upload" to "هل ترغب فعلًا في تنفيذ الرفع؟",
"start_upload" to "بدء الرفع؟"
),
"ROMANIAN" to mapOf(
@ -3392,7 +3432,12 @@ object LanguageManager {
"done" to "Finalizat",
"not_done" to "Nefinalizat",
"none" to "Nimic",
"view_missing" to "Vizualizare lipsă: %s"
"view_missing" to "Vizualizare lipsă: %s",
"session_over_12" to "Sesiunea rulează de mai bine de 12 ore.",
"cancel" to "Anulează",
"ok" to "OK",
"ask_before_upload" to "Vrei într-adevăr să efectuezi încărcarea?",
"start_upload" to "Pornești încărcarea?"
),
"SPANISH" to mapOf(
@ -3767,7 +3812,12 @@ object LanguageManager {
"done" to "Completado",
"not_done" to "No completado",
"none" to "Ninguno",
"view_missing" to "Vista faltante: %s"
"view_missing" to "Vista faltante: %s",
"session_over_12" to "La sesión lleva más de 12 horas.",
"cancel" to "Cancelar",
"ok" to "OK",
"ask_before_upload" to "¿Realmente deseas realizar la carga?",
"start_upload" to "¿Iniciar la carga?"
)
)
}

View File

@ -1,11 +1,13 @@
package com.dano.test1
import android.app.AlertDialog
import android.content.Context
import android.util.Log
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import android.view.LayoutInflater
import android.widget.EditText
import android.widget.LinearLayout
import android.widget.Toast
import kotlinx.coroutines.*
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
@ -14,6 +16,7 @@ import org.json.JSONObject
object LoginManager {
private const val SERVER_LOGIN_URL = "https://daniel-ocks.de/qdb/login.php"
private const val SERVER_CHANGE_URL = "https://daniel-ocks.de/qdb/change_password.php"
private val client = OkHttpClient()
fun loginUserWithCredentials(
@ -39,23 +42,177 @@ object LoginManager {
val response = client.newCall(request).execute()
val text = response.body?.string()
if (response.isSuccessful && text != null) {
val json = JSONObject(text)
if (json.optBoolean("success")) {
val token = json.getString("token")
// => setzt auch den Login-Timestamp:
TokenStore.save(context, token, username)
withContext(Dispatchers.Main) { onSuccess(token) }
} else {
withContext(Dispatchers.Main) { onError("Login fehlgeschlagen") }
}
} else {
if (!response.isSuccessful || text == null) {
withContext(Dispatchers.Main) { onError("Fehler beim Login (${response.code})") }
return@launch
}
val json = JSONObject(text)
if (!json.optBoolean("success")) {
withContext(Dispatchers.Main) { onError(json.optString("message", "Login fehlgeschlagen")) }
return@launch
}
// Passwortwechsel erforderlich?
if (json.optBoolean("must_change_password", false)) {
withContext(Dispatchers.Main) {
showChangePasswordDialog(
context = context,
username = username,
oldPassword = password,
onChanged = { token ->
// Nach PW-Änderung direkt eingeloggt
TokenStore.save(context, token, username)
onSuccess(token)
},
onError = onError
)
}
return@launch
}
// normaler Login: Token speichern
val token = json.getString("token")
TokenStore.save(context, token, username)
withContext(Dispatchers.Main) { onSuccess(token) }
} catch (e: Exception) {
Log.e("LOGIN", "Exception", e)
withContext(Dispatchers.Main) { onError("Exception: ${e.message}") }
}
}
}
private fun showChangePasswordDialog(
context: Context,
username: String,
oldPassword: String,
onChanged: (String) -> Unit,
onError: (String) -> Unit
) {
val container = LinearLayout(context).apply {
orientation = LinearLayout.VERTICAL
setPadding(48, 24, 48, 0)
}
val etNew = EditText(context).apply {
hint = "Neues Passwort"
inputType = android.text.InputType.TYPE_CLASS_TEXT or
android.text.InputType.TYPE_TEXT_VARIATION_PASSWORD
}
val etRepeat = EditText(context).apply {
hint = "Neues Passwort (wiederholen)"
inputType = android.text.InputType.TYPE_CLASS_TEXT or
android.text.InputType.TYPE_TEXT_VARIATION_PASSWORD
}
container.addView(etNew)
container.addView(etRepeat)
val dialog = AlertDialog.Builder(context)
.setTitle("Passwort ändern")
.setMessage("Du verwendest ein Standard-Konto. Bitte setze jetzt ein eigenes Passwort.")
.setView(container)
.setPositiveButton("OK", null) // nicht sofort schließen lassen
.setNegativeButton("Abbrechen", null) // nicht sofort schließen lassen
.setCancelable(false)
.create()
dialog.setOnShowListener {
val btnOk = dialog.getButton(AlertDialog.BUTTON_POSITIVE)
val btnCancel = dialog.getButton(AlertDialog.BUTTON_NEGATIVE)
btnOk.setOnClickListener {
etNew.error = null
etRepeat.error = null
val p1 = etNew.text?.toString().orEmpty()
val p2 = etRepeat.text?.toString().orEmpty()
when {
p1.length < 6 -> {
etNew.error = "Mindestens 6 Zeichen."
return@setOnClickListener
}
p1 != p2 -> {
etRepeat.error = "Passwörter stimmen nicht überein."
return@setOnClickListener
}
else -> {
btnOk.isEnabled = false
btnCancel.isEnabled = false
changePassword(
context = context,
username = username,
oldPassword = oldPassword,
newPassword = p1,
onChanged = { token ->
dialog.dismiss()
onChanged(token)
},
onError = { msg ->
btnOk.isEnabled = true
btnCancel.isEnabled = true
Toast.makeText(context, msg, Toast.LENGTH_LONG).show()
}
)
}
}
}
// >>> Überarbeitet: Abbrechen schließt Dialog und informiert den Aufrufer
btnCancel.setOnClickListener {
dialog.dismiss()
onError("Passwortänderung abgebrochen.")
}
}
dialog.show()
}
private fun changePassword(
context: Context,
username: String,
oldPassword: String,
newPassword: String,
onChanged: (String) -> Unit,
onError: (String) -> Unit
) {
CoroutineScope(Dispatchers.IO).launch {
try {
val body = JSONObject()
.put("username", username)
.put("old_password", oldPassword)
.put("new_password", newPassword)
.toString()
.toRequestBody("application/json".toMediaType())
val req = Request.Builder()
.url(SERVER_CHANGE_URL)
.post(body)
.build()
val resp = client.newCall(req).execute()
val txt = resp.body?.string()
if (!resp.isSuccessful || txt == null) {
withContext(Dispatchers.Main) { onError("Fehler beim Ändern (${resp.code})") }
return@launch
}
val json = JSONObject(txt)
if (!json.optBoolean("success")) {
withContext(Dispatchers.Main) { onError(json.optString("message", "Ändern fehlgeschlagen")) }
return@launch
}
val token = json.getString("token")
withContext(Dispatchers.Main) { onChanged(token) }
} catch (e: Exception) {
Log.e("LOGIN", "changePassword Exception", e)
withContext(Dispatchers.Main) { onError("Exception: ${e.message}") }
}
}
}
}