added username and password for up- and download

This commit is contained in:
oxidiert
2025-09-23 16:10:19 +02:00
parent 91f6f77b73
commit 5f5c766133
5 changed files with 145 additions and 109 deletions

View File

@ -26,10 +26,14 @@ object DatabaseUploader {
private val client = OkHttpClient()
fun uploadDatabaseWithLogin(context: Context, password: String) {
LoginManager.loginUser(context, password,
/** NEU: Login mit Username+Password, danach Upload wie gehabt */
fun uploadDatabaseWithLogin(context: Context, username: String, password: String) {
LoginManager.loginUserWithCredentials(
context = context,
username = username,
password = password,
onSuccess = { token ->
Log.d("UPLOAD", "Login OK")
Log.d("UPLOAD", "Login OK (user=$username)")
uploadDatabase(context, token)
},
onError = { msg -> Log.e("UPLOAD", "Login fehlgeschlagen: $msg") }
@ -47,13 +51,12 @@ object DatabaseUploader {
try {
val db = SQLiteDatabase.openDatabase(dbFile.absolutePath, null, SQLiteDatabase.OPEN_READWRITE)
db.rawQuery("PRAGMA wal_checkpoint(FULL);", null).use { /* ignore */ }
db.rawQuery("PRAGMA wal_checkpoint(FULL);", null).use { /* noop */ }
db.close()
} catch (_: Exception) { }
checkDatabaseExists() // nur Logging
checkDatabaseExists()
uploadPseudoDelta(context, dbFile, token)
} catch (e: Exception) {
Log.e("UPLOAD", "Fehler", e)
}
@ -99,7 +102,7 @@ object DatabaseUploader {
.addFormDataPart("file", "payload.enc", tmpEnc.asRequestBody("application/octet-stream".toMediaType()))
.build()
val request = Request.Builder().url(SERVER_DELTA_URL).post(body).build()
val request = Request.Builder().url("http://49.13.157.44/uploadDeltaTest5.php").post(body).build()
client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
Log.e("UPLOAD", "Fehlgeschlagen: ${e.message}")
@ -109,8 +112,17 @@ object DatabaseUploader {
val respBody = try { response.body?.string() ?: "" } catch (_: Exception) { "" }
if (response.isSuccessful) {
Log.d("UPLOAD", "OK: $respBody")
if (!file.delete()) Log.w("UPLOAD", "Lokale DB nicht gelöscht.")
File(file.parent, file.name + "-journal").delete()
// <<< alte Logik wieder aktivieren: lokale DB + Neben­dateien löschen
try {
if (!file.delete()) Log.w("UPLOAD", "Lokale DB nicht gelöscht.")
File(file.parent, "${file.name}-journal").delete()
File(file.parent, "${file.name}-wal").delete()
File(file.parent, "${file.name}-shm").delete()
} catch (e: Exception) {
Log.w("UPLOAD", "Fehler beim Löschen lokaler DB-Dateien", e)
}
// >>>
} else {
Log.e("UPLOAD", "HTTP ${response.code}: $respBody")
}

View File

@ -60,16 +60,13 @@ class HandlerGlassScaleQuestion(
setTextSizePercentOfScreenHeight(titleTv, 0.03f)
setTextSizePercentOfScreenHeight(questionTv, 0.03f)
// --- FIXE ICON-LEISTE AUFBAUEN (bleibt stehen) ---
// ----- feste Icon-Leiste -----
val header = layout.findViewById<LinearLayout>(R.id.glass_header)
header.removeAllViews()
// linker Platzhalter (entspricht der Symptom-Spalte mit weight=4)
header.addView(Space(context).apply {
layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 4f)
})
val iconSizePx = (context.resources.displayMetrics.density * 36).toInt() // ~36dp
val iconSizePx = (context.resources.displayMetrics.density * 36).toInt()
scaleLabels.forEach { labelKey ->
val cell = FrameLayout(context).apply {
layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)
@ -83,51 +80,44 @@ class HandlerGlassScaleQuestion(
cell.addView(img)
header.addView(cell)
}
// ---------------------------------------------------
// -----------------------------
val tableLayout = layout.findViewById<TableLayout>(R.id.glass_table)
tableLayout.removeAllViews()
addSymptomRows(tableLayout)
// ggf. Antworten aus DB wiederherstellen
val anySymptomNeedsRestore = question.symptoms.any { !answers.containsKey(it) }
if (anySymptomNeedsRestore) {
CoroutineScope(Dispatchers.IO).launch {
try {
val clientCode = GlobalValues.LAST_CLIENT_CODE
if (clientCode.isNullOrBlank()) return@launch
val clientCode = GlobalValues.LAST_CLIENT_CODE ?: return@launch
val allAnswersForClient = MyApp.database.answerDao().getAnswersForClient(clientCode)
val answerMap = allAnswersForClient.associateBy({ it.questionId }, { it.answerValue })
withContext(Dispatchers.Main) {
val table = tableLayout
for ((index, symptomKey) in question.symptoms.withIndex()) {
val answerMapKey = questionnaireMeta + "-" + symptomKey
val answerMapKey = "$questionnaireMeta-$symptomKey"
val dbAnswer = answerMap[answerMapKey]?.takeIf { it.isNotBlank() }?.trim()
if (!answers.containsKey(symptomKey) && !dbAnswer.isNullOrBlank()) {
val rowIndex = index // erste Datenzeile ist Index 1 im TableLayout-Aufbau unten
if (rowIndex < table.childCount) {
val row = table.getChildAt(rowIndex) as? TableRow ?: continue
if (index < table.childCount) {
val row = table.getChildAt(index) as? TableRow ?: continue
val radioGroup = row.getChildAt(1) as? RadioGroup ?: continue
for (i in 0 until radioGroup.childCount) {
val cell = radioGroup.getChildAt(i) as? FrameLayout ?: continue
val rb = cell.getChildAt(0) as? RadioButton ?: continue
val rb = getRadioFromChild(radioGroup.getChildAt(i)) ?: continue
if ((rb.tag as? String)?.trim() == dbAnswer) {
rb.isChecked = true
break
}
}
answers[symptomKey] = dbAnswer
val point = pointsMap[dbAnswer] ?: 0
points.add(point)
points.add(pointsMap[dbAnswer] ?: 0)
}
}
}
}
} catch (e: Exception) {
e.printStackTrace()
}
} catch (_: Exception) { /* ignore */ }
}
}
@ -166,25 +156,21 @@ class HandlerGlassScaleQuestion(
layoutParams = TableRow.LayoutParams(0, TableRow.LayoutParams.WRAP_CONTENT, 5f)
}
// WICHTIG: RadioButtons sind direkte Kinder des RadioGroup!
scaleLabels.forEach { labelKey ->
val cell = FrameLayout(context).apply {
layoutParams = RadioGroup.LayoutParams(0, RadioGroup.LayoutParams.WRAP_CONTENT, 1f)
}
val radioButton = RadioButton(context).apply {
val rb = RadioButton(context).apply {
tag = labelKey
id = View.generateViewId()
isChecked = savedLabel == labelKey
layoutParams = FrameLayout.LayoutParams(
FrameLayout.LayoutParams.WRAP_CONTENT,
FrameLayout.LayoutParams.WRAP_CONTENT,
Gravity.CENTER
)
gravity = Gravity.CENTER
setPadding(0, 0, 0, 0)
}
cell.addView(radioButton)
radioGroup.addView(cell)
val lp = RadioGroup.LayoutParams(
0, RadioGroup.LayoutParams.WRAP_CONTENT, 1f
).apply { gravity = Gravity.CENTER }
rb.layoutParams = lp
radioGroup.addView(rb)
}
row.addView(radioGroup)
table.addView(row)
}
@ -197,8 +183,7 @@ class HandlerGlassScaleQuestion(
val radioGroup = row.getChildAt(1) as RadioGroup
var anyChecked = false
for (j in 0 until radioGroup.childCount) {
val cell = radioGroup.getChildAt(j) as FrameLayout
val rb = cell.getChildAt(0) as RadioButton
val rb = getRadioFromChild(radioGroup.getChildAt(j)) ?: continue
if (rb.isChecked) { anyChecked = true; break }
}
if (!anyChecked) return false
@ -207,9 +192,10 @@ class HandlerGlassScaleQuestion(
}
override fun saveAnswer() {
// alte Punkte entfernen
question.symptoms.forEach { key ->
val previousLabel = answers[key] as? String
previousLabel?.let { lbl -> pointsMap[lbl] }?.let { points.remove(it) }
val prev = answers[key] as? String
prev?.let { pointsMap[it] }?.let { points.remove(it) }
}
val table = layout.findViewById<TableLayout>(R.id.glass_table)
@ -218,18 +204,25 @@ class HandlerGlassScaleQuestion(
val symptomKey = question.symptoms[i]
val radioGroup = row.getChildAt(1) as RadioGroup
for (j in 0 until radioGroup.childCount) {
val cell = radioGroup.getChildAt(j) as FrameLayout
val rb = cell.getChildAt(0) as RadioButton
val rb = getRadioFromChild(radioGroup.getChildAt(j)) ?: continue
if (rb.isChecked) {
val selectedLabel = rb.tag as String
answers[symptomKey] = selectedLabel
points.add(pointsMap[selectedLabel] ?: 0)
val selected = rb.tag as String
answers[symptomKey] = selected
points.add(pointsMap[selected] ?: 0)
break
}
}
}
}
// --- Helpers ---
private fun getRadioFromChild(child: View): RadioButton? =
when (child) {
is RadioButton -> child
is FrameLayout -> child.getChildAt(0) as? RadioButton
else -> null
}
private fun setTextSizePercentOfScreenHeight(view: TextView, percentOfHeight: Float) {
val dm = (view.context ?: layout.context).resources.displayMetrics
val sp = (dm.heightPixels * percentOfHeight) / dm.scaledDensity

View File

@ -434,59 +434,80 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
private fun setupUploadButton() {
uploadButton.text = t("upload")
uploadButton.setOnClickListener {
val clientCode = editText.text.toString().trim()
GlobalValues.LAST_CLIENT_CODE = clientCode
val input = EditText(activity).apply { hint = "Server-Passwort" }
android.app.AlertDialog.Builder(activity)
.setTitle(t("login_required"))
.setView(input)
.setPositiveButton("OK") { _, _ ->
val password = input.text.toString()
if (password.isNotBlank()) {
Toast.makeText(activity, t("checking_login"), Toast.LENGTH_SHORT).show()
DatabaseUploader.uploadDatabaseWithLogin(activity, password)
} else {
Toast.makeText(activity, t("enter_password"), Toast.LENGTH_SHORT).show()
}
GlobalValues.LAST_CLIENT_CODE = editText.text.toString().trim()
promptCredentials(
title = t("login_required") ?: "Login erforderlich",
onOk = { user, pass ->
// Login -> Upload
DatabaseUploader.uploadDatabaseWithLogin(activity, user, pass)
}
.setNegativeButton(t("cancel"), null)
.show()
)
}
}
private fun setupDownloadButton() {
downloadButton.text = t("download")
downloadButton.setOnClickListener {
val clientCode = editText.text.toString().trim()
GlobalValues.LAST_CLIENT_CODE = clientCode
val input = EditText(activity).apply { hint = "Server-Passwort" }
android.app.AlertDialog.Builder(activity)
.setTitle(t("login_required"))
.setView(input)
.setPositiveButton("OK") { _, _ ->
val password = input.text.toString()
if (password.isNotBlank()) {
LoginManager.loginUser(
context = activity,
password = password,
onSuccess = { token ->
Toast.makeText(activity, t("login_ok"), Toast.LENGTH_SHORT).show()
DatabaseDownloader.downloadAndReplaceDatabase(activity, token)
updateMainButtonsState(true)
},
onError = { error ->
Toast.makeText(activity, error, Toast.LENGTH_LONG).show()
}
)
} else {
Toast.makeText(activity, t("enter_password"), Toast.LENGTH_SHORT).show()
}
GlobalValues.LAST_CLIENT_CODE = editText.text.toString().trim()
promptCredentials(
title = t("login_required") ?: "Login erforderlich",
onOk = { user, pass ->
// Login -> Token -> Download
LoginManager.loginUserWithCredentials(
context = activity,
username = user,
password = pass,
onSuccess = { token ->
Toast.makeText(activity, t("login_ok") ?: "Login OK", Toast.LENGTH_SHORT).show()
DatabaseDownloader.downloadAndReplaceDatabase(activity, token)
updateMainButtonsState(true)
},
onError = { error ->
Toast.makeText(activity, error, Toast.LENGTH_LONG).show()
}
)
}
.setNegativeButton(t("cancel"), null)
.show()
)
}
}
private fun promptCredentials(
title: String,
onOk: (username: String, password: String) -> Unit
) {
val wrapper = LinearLayout(activity).apply {
orientation = LinearLayout.VERTICAL
setPadding(dp(20), dp(8), dp(20), 0)
}
val etUser = EditText(activity).apply {
hint = "Username"
setSingleLine()
}
val etPass = EditText(activity).apply {
hint = "Passwort"
setSingleLine()
inputType = android.text.InputType.TYPE_CLASS_TEXT or
android.text.InputType.TYPE_TEXT_VARIATION_PASSWORD
}
wrapper.addView(etUser)
wrapper.addView(etPass)
android.app.AlertDialog.Builder(activity)
.setTitle(title)
.setView(wrapper)
.setPositiveButton("OK") { _, _ ->
val u = etUser.text.toString().trim()
val p = etPass.text.toString()
if (u.isNotEmpty() && p.isNotEmpty()) {
onOk(u, p)
} else {
Toast.makeText(activity, t("enter_password") ?: "Bitte Username & Passwort eingeben", Toast.LENGTH_SHORT).show()
}
}
.setNegativeButton(t("cancel") ?: "Abbrechen", null)
.show()
}
private fun setupDatabaseButtonHandler() {
DatabaseButtonHandler(
activity = activity,

View File

@ -19,7 +19,7 @@ object LanguageManager {
private fun injectDynamicValues(text: String): String {
val points = RHS_POINTS ?: 0
val color = when (points) {
in 1..12 -> "#4CAF50" // Grün
in 0..12 -> "#4CAF50" // Grün
in 13..36 -> "#FFEB3B" // Gelb
in 37..100 -> "#F44336" // Rot
else -> "#9E9E9E" // Grau (Standard)

View File

@ -17,35 +17,45 @@ object LoginManager {
private const val SERVER_LOGIN_URL = "http://49.13.157.44/login.php"
private val client = OkHttpClient()
fun loginUser(
/**
* Neuer Login: Benutzername + Passwort -> Token
*/
fun loginUserWithCredentials(
context: Context,
username: String,
password: String,
onSuccess: (String) -> Unit,
onError: (String) -> Unit
) {
CoroutineScope(Dispatchers.IO).launch {
try {
val requestBody = """{"password":"$password"}"""
.toRequestBody("application/json".toMediaType())
val bodyJson = JSONObject().apply {
put("username", username.trim())
put("password", password)
}.toString()
val requestBody = bodyJson.toRequestBody("application/json".toMediaType())
val request = Request.Builder()
.url(SERVER_LOGIN_URL)
.post(requestBody)
.build()
val response = client.newCall(request).execute()
val responseText = response.body?.string()
if (response.isSuccessful && responseText != null) {
val json = JSONObject(responseText)
if (json.optBoolean("success")) {
val token = json.getString("token")
withContext(Dispatchers.Main) { onSuccess(token) }
} else {
withContext(Dispatchers.Main) { onError("Login fehlgeschlagen") }
client.newCall(request).execute().use { resp ->
val text = resp.body?.string()
if (!resp.isSuccessful || text == null) {
withContext(Dispatchers.Main) {
onError("Fehler beim Login (${resp.code})")
}
return@use
}
val json = runCatching { JSONObject(text) }.getOrNull()
val ok = json?.optBoolean("success") == true
val token = json?.optString("token").orEmpty()
withContext(Dispatchers.Main) {
if (ok && token.isNotBlank()) onSuccess(token)
else onError(json?.optString("message") ?: "Login fehlgeschlagen")
}
} else {
withContext(Dispatchers.Main) { onError("Fehler beim Login (${response.code})") }
}
} catch (e: Exception) {
Log.e("LOGIN", "Exception", e)