diff --git a/app/src/main/java/com/dano/test1/DatabaseUploader.kt b/app/src/main/java/com/dano/test1/DatabaseUploader.kt index 1af58fa..abf6c84 100644 --- a/app/src/main/java/com/dano/test1/DatabaseUploader.kt +++ b/app/src/main/java/com/dano/test1/DatabaseUploader.kt @@ -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") } diff --git a/app/src/main/java/com/dano/test1/HandlerGlassScaleQuestion.kt b/app/src/main/java/com/dano/test1/HandlerGlassScaleQuestion.kt index 9a5d46f..8bc4fbc 100644 --- a/app/src/main/java/com/dano/test1/HandlerGlassScaleQuestion.kt +++ b/app/src/main/java/com/dano/test1/HandlerGlassScaleQuestion.kt @@ -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(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(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(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 diff --git a/app/src/main/java/com/dano/test1/HandlerOpeningScreen.kt b/app/src/main/java/com/dano/test1/HandlerOpeningScreen.kt index ba5e2f9..36aea76 100644 --- a/app/src/main/java/com/dano/test1/HandlerOpeningScreen.kt +++ b/app/src/main/java/com/dano/test1/HandlerOpeningScreen.kt @@ -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, diff --git a/app/src/main/java/com/dano/test1/LanguageManager.kt b/app/src/main/java/com/dano/test1/LanguageManager.kt index 9a1b154..e9e0a44 100644 --- a/app/src/main/java/com/dano/test1/LanguageManager.kt +++ b/app/src/main/java/com/dano/test1/LanguageManager.kt @@ -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) diff --git a/app/src/main/java/com/dano/test1/LoginManager.kt b/app/src/main/java/com/dano/test1/LoginManager.kt index 42186a5..4133fdd 100644 --- a/app/src/main/java/com/dano/test1/LoginManager.kt +++ b/app/src/main/java/com/dano/test1/LoginManager.kt @@ -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)