diff --git a/app/src/main/java/com/dano/test1/DatabaseDownloader.kt b/app/src/main/java/com/dano/test1/DatabaseDownloader.kt index 107c37f..ad5b4b7 100644 --- a/app/src/main/java/com/dano/test1/DatabaseDownloader.kt +++ b/app/src/main/java/com/dano/test1/DatabaseDownloader.kt @@ -16,21 +16,24 @@ import javax.crypto.spec.SecretKeySpec object DatabaseDownloader { private const val DB_NAME = "questionnaire_database" - private const val API_TOKEN = "MEIN_SUPER_GEHEIMES_TOKEN_12345" - private const val SERVER_DOWNLOAD_URL = "http://49.13.157.44/downloadFull.php?token=$API_TOKEN" + private const val SERVER_DOWNLOAD_URL = "http://49.13.157.44/downloadFull.php" // AES-256 Key (muss exakt 32 Bytes lang sein) private const val AES_KEY = "12345678901234567890123456789012" private val client = OkHttpClient() - fun downloadAndReplaceDatabase(context: Context) { + /** + * Startet den Download und Austausch der DB, benötigt gültiges Token + */ + fun downloadAndReplaceDatabase(context: Context, token: String) { CoroutineScope(Dispatchers.IO).launch { try { Log.d("DOWNLOAD", "Download gestartet: $SERVER_DOWNLOAD_URL") val request = Request.Builder() .url(SERVER_DOWNLOAD_URL) + .header("Authorization", "Bearer $token") .build() val response = client.newCall(request).execute() diff --git a/app/src/main/java/com/dano/test1/DatabaseUploader.kt b/app/src/main/java/com/dano/test1/DatabaseUploader.kt index a8606b9..e82bee6 100644 --- a/app/src/main/java/com/dano/test1/DatabaseUploader.kt +++ b/app/src/main/java/com/dano/test1/DatabaseUploader.kt @@ -20,14 +20,32 @@ import kotlin.system.exitProcess object DatabaseUploader { private const val DB_NAME = "questionnaire_database" - // TODO entferne uploadDeltaTest2.php - private const val SERVER_DELTA_URL = "http://49.13.157.44/uploadDeltaTest3.php" + private const val SERVER_DELTA_URL = "http://49.13.157.44/uploadDeltaTest4.php" private const val SERVER_CHECK_URL = "http://49.13.157.44/checkDatabaseExists.php" - private const val API_TOKEN = "MEIN_SUPER_GEHEIMES_TOKEN_12345" private val client = OkHttpClient() - fun uploadDatabase(context: Context) { + /** + * Startet den Upload mit Login über LoginManager. + * @param context Android Context + * @param password Vom User eingegebenes Passwort + */ + fun uploadDatabaseWithLogin(context: Context, password: String) { + LoginManager.loginUser(context, password, + onSuccess = { token -> + Log.d("UPLOAD", "Login erfolgreich, Token erhalten") + uploadDatabase(context, token) + }, + onError = { errorMsg -> + Log.e("UPLOAD", "Login fehlgeschlagen: $errorMsg") + } + ) + } + + /** + * Interner Upload, benötigt gültiges Token + */ + private fun uploadDatabase(context: Context, token: String) { CoroutineScope(Dispatchers.IO).launch { try { val dbFile = context.getDatabasePath(DB_NAME) @@ -45,9 +63,7 @@ object DatabaseUploader { ) db.rawQuery("PRAGMA wal_checkpoint(FULL);", null).use { cursor -> if (cursor.moveToFirst()) { - try { - Log.d("UPLOAD", "WAL-Checkpoint result: ${cursor.getInt(0)}") - } catch (_: Exception) {} + try { Log.d("UPLOAD", "WAL-Checkpoint result: ${cursor.getInt(0)}") } catch (_: Exception) {} } } db.close() @@ -59,12 +75,12 @@ object DatabaseUploader { val exists = checkDatabaseExists() if (exists) { Log.d("UPLOAD", "Server-Datenbank vorhanden → Delta-Upload") - uploadPseudoDelta(context, dbFile) } else { Log.d("UPLOAD", "Keine Server-Datenbank → Delta-Upload") - uploadPseudoDelta(context, dbFile) } + uploadPseudoDelta(context, dbFile, token) + } catch (e: Exception) { Log.e("UPLOAD", "Fehler beim Hochladen der DB", e) } @@ -97,15 +113,7 @@ object DatabaseUploader { } } - /** - * Wichtig: Diese Funktion wurde erweitert, sodass: - * - die DB als JSON in eine temporäre Datei geschrieben wird, - * - diese JSON-Datei AES-verschlüsselt wird (mit AES256Helper.encryptFile), - * - die verschlüsselte Datei als Multipart 'file' an den Server gesendet wird. - * - * (Funktionalität: gleiche Signatur wie vorher behalten) - */ - private fun uploadPseudoDelta(context: Context, file: File) { + private fun uploadPseudoDelta(context: Context, file: File, token: String) { try { val db = SQLiteDatabase.openDatabase(file.absolutePath, null, SQLiteDatabase.OPEN_READONLY) @@ -125,25 +133,21 @@ object DatabaseUploader { db.close() - // Schreibe JSON in temporäre Datei val tmpJson = File(context.cacheDir, "payload.json") tmpJson.writeText(data.toString()) - // Verschlüssele JSON -> tmpEnc val tmpEnc = File(context.cacheDir, "payload.enc") try { AES256Helper.encryptFile(tmpJson, tmpEnc) } catch (e: Exception) { Log.e("UPLOAD", "Fehler bei der Verschlüsselung der JSON-Datei", e) - // cleanup tmpJson.delete() return } val requestBody = MultipartBody.Builder() .setType(MultipartBody.FORM) - .addFormDataPart("token", API_TOKEN) - // Datei-Feld "file" mit verschlüsselter Payload + .addFormDataPart("token", token) // Token vom Login .addFormDataPart( "file", "payload.enc", @@ -159,31 +163,19 @@ object DatabaseUploader { client.newCall(request).enqueue(object : Callback { override fun onFailure(call: Call, e: IOException) { Log.e("UPLOAD", "Delta-Upload fehlgeschlagen: ${e.message}") - // cleanup tmpJson.delete() tmpEnc.delete() } override fun onResponse(call: Call, response: Response) { - val body = try { - response.body?.string() ?: "Keine Response" - } catch (e: Exception) { + val body = try { response.body?.string() ?: "Keine Response" } catch (e: Exception) { "Fehler beim Lesen der Response: ${e.message}" } if (response.isSuccessful) { Log.d("UPLOAD", "Delta-Upload erfolgreich: $body") - // Lösche Hauptdatenbank - if (file.delete()) { - Log.d("UPLOAD", "Lokale DB gelöscht.") - } else { - Log.e("UPLOAD", "Löschen der lokalen DB fehlgeschlagen.") - } - // Lösche Journal-Datei + if (file.delete()) Log.d("UPLOAD", "Lokale DB gelöscht.") else Log.e("UPLOAD", "Löschen der lokalen DB fehlgeschlagen.") val journalFile = File(file.parent, file.name + "-journal") - if (journalFile.exists() && journalFile.delete()) { - Log.d("UPLOAD", "Journal-Datei gelöscht.") - } - // cleanup temp files + if (journalFile.exists() && journalFile.delete()) Log.d("UPLOAD", "Journal-Datei gelöscht.") tmpJson.delete() tmpEnc.delete() exitProcess(0) diff --git a/app/src/main/java/com/dano/test1/HandlerClientCoachCode.kt b/app/src/main/java/com/dano/test1/HandlerClientCoachCode.kt index ba363d6..d56d276 100644 --- a/app/src/main/java/com/dano/test1/HandlerClientCoachCode.kt +++ b/app/src/main/java/com/dano/test1/HandlerClientCoachCode.kt @@ -69,10 +69,9 @@ class HandlerClientCoachCode( val clientCode = clientCodeField.text.toString() val coachCode = coachCodeField.text.toString() - // Prüfen, ob die Datenbank-Dateien vor dem Klick existieren - val dbFile = layout.context.getDatabasePath("questionnaire_database") - val dbJournalFile = layout.context.getDatabasePath("questionnaire_database-journal") - val dbExisted = dbFile.exists() || dbJournalFile.exists() + // Prüfen, ob die DB-Datei vor dem Zugriff existiert + val dbPath = layout.context.getDatabasePath("questionnaire_database") + val dbExistedBefore = dbPath.exists() // Check if client code already exists asynchronously CoroutineScope(Dispatchers.IO).launch { @@ -86,19 +85,21 @@ class HandlerClientCoachCode( } else { // Either no existing client or re-using previous code saveAnswers(clientCode, coachCode) - - // Datenbank-Dateien löschen, wenn sie vorher NICHT existierten - if (!dbExisted) { - dbFile.delete() - dbJournalFile.delete() - } - goToNextQuestion() + + // Lösche DB-Dateien nur, wenn sie vorher nicht existierten + if (!dbExistedBefore) { + MyApp.database.close() + dbPath.delete() + val journalFile = layout.context.getDatabasePath("questionnaire_database-journal") + journalFile.delete() + } } } } } + // Handle Previous button click private fun onPreviousClicked(clientCodeField: EditText, coachCodeField: EditText) { val clientCode = clientCodeField.text.toString() diff --git a/app/src/main/java/com/dano/test1/HandlerOpeningScreen.kt b/app/src/main/java/com/dano/test1/HandlerOpeningScreen.kt index fb897df..66f03f9 100644 --- a/app/src/main/java/com/dano/test1/HandlerOpeningScreen.kt +++ b/app/src/main/java/com/dano/test1/HandlerOpeningScreen.kt @@ -13,7 +13,7 @@ import android.util.Log import com.dano.test1.data.CompletedQuestionnaire import java.io.File -var INTEGRATION_INDEX_POINTS: Int? = null +var RHS_POINTS: Int? = null class HandlerOpeningScreen(private val activity: MainActivity) { @@ -342,13 +342,12 @@ class HandlerOpeningScreen(private val activity: MainActivity) { MyApp.database.completedQuestionnaireDao().getAllForClient(clientCode) } - // fülle buttonPoints & INTEGRATION_INDEX_POINTS buttonPoints.clear() for (entry in completedEntries) { if (entry.isDone) { buttonPoints[entry.questionnaireId] = entry.sumPoints ?: 0 - if (entry.questionnaireId.contains("questionnaire_3_integration_index", ignoreCase = true)) { - INTEGRATION_INDEX_POINTS = entry.sumPoints + if (entry.questionnaireId.contains("questionnaire_2_rhs", ignoreCase = true)) { + RHS_POINTS = entry.sumPoints } } } @@ -682,39 +681,79 @@ class HandlerOpeningScreen(private val activity: MainActivity) { uploadButton.setOnClickListener { val clientCode = editText.text.toString().trim() + if (clientCode.isBlank()) { + val message = LanguageManager.getText(languageID, "please_client_code") + Toast.makeText(activity, message, Toast.LENGTH_SHORT).show() + return@setOnClickListener + } + GlobalValues.LAST_CLIENT_CODE = clientCode - Toast.makeText(activity, "Datenbank wird hochgeladen...", Toast.LENGTH_SHORT).show() - DatabaseUploader.uploadDatabase(activity) + // Passwort-Eingabe-Popup + val input = EditText(activity).apply { + hint = "Server-Passwort" + } + + android.app.AlertDialog.Builder(activity) + .setTitle("Login erforderlich") + .setView(input) + .setPositiveButton("OK") { _, _ -> + val password = input.text.toString() + if (password.isNotBlank()) { + Toast.makeText(activity, "Login wird überprüft...", Toast.LENGTH_SHORT).show() + // Login + Upload starten + DatabaseUploader.uploadDatabaseWithLogin(activity, password) + } else { + Toast.makeText(activity, "Bitte Passwort eingeben", Toast.LENGTH_SHORT).show() + } + } + .setNegativeButton("Abbrechen", null) + .show() } } - // --- Füge diese Funktion in deine Klasse ein --- - private fun isDatabasePopulated(): Boolean { - return try { - val db = MyApp.database.openHelper.readableDatabase - val cursor = db.query( - "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name != 'room_master_table'" - ) - cursor.use { it.count > 0 } - } catch (e: Exception) { - false - } - } + private fun setupDownloadButton() { downloadButton.text = "Download" downloadButton.setOnClickListener { val clientCode = editText.text.toString().trim() - GlobalValues.LAST_CLIENT_CODE = clientCode - Toast.makeText(activity, "Datenbank wird heruntergeladen...", Toast.LENGTH_SHORT).show() - DatabaseDownloader.downloadAndReplaceDatabase(activity) - updateMainButtonsState(true) + // Eingabe-Popup für Passwort anzeigen + val input = EditText(activity).apply { + hint = "Server-Passwort" + } + + android.app.AlertDialog.Builder(activity) + .setTitle("Login erforderlich") + .setView(input) + .setPositiveButton("OK") { _, _ -> + val password = input.text.toString() + if (password.isNotBlank()) { + // Login starten + LoginManager.loginUser( + context = activity, + password = password, + onSuccess = { token -> + Toast.makeText(activity, "Login erfolgreich", Toast.LENGTH_SHORT).show() + DatabaseDownloader.downloadAndReplaceDatabase(activity, token) + updateMainButtonsState(true) + }, + onError = { error -> + Toast.makeText(activity, error, Toast.LENGTH_LONG).show() + } + ) + } else { + Toast.makeText(activity, "Bitte Passwort eingeben", Toast.LENGTH_SHORT).show() + } + } + .setNegativeButton("Abbrechen", null) + .show() } } + private fun updateMainButtonsState(isDatabaseAvailable: Boolean) { val buttons = listOf(buttonLoad, saveButton, editButton) buttons.forEach { button -> diff --git a/app/src/main/java/com/dano/test1/LanguageManager.kt b/app/src/main/java/com/dano/test1/LanguageManager.kt index a61a6bb..e7bd420 100644 --- a/app/src/main/java/com/dano/test1/LanguageManager.kt +++ b/app/src/main/java/com/dano/test1/LanguageManager.kt @@ -17,7 +17,7 @@ object LanguageManager { } private fun injectDynamicValues(text: String): String { - val points = INTEGRATION_INDEX_POINTS ?: 0 + val points = RHS_POINTS ?: 0 val color = when (points) { in 1..12 -> "#4CAF50" // Grün in 13..36 -> "#FFEB3B" // Gelb @@ -26,7 +26,7 @@ object LanguageManager { } val coloredPoints = "$points" - return text.replace("INTEGRATION_INDEX_POINTS", coloredPoints) + return text.replace("RHS_POINTS", coloredPoints) } // Sprachdatenbank: Map> @@ -285,7 +285,7 @@ object LanguageManager { "select_one_answer_per_row" to "Bitte wählen Sie eine Antwort pro Reihe aus!", "no_next_question_defined" to "Keine Weiterleitungsseite definiert", "date_consultation_health_interview_result" to "Datum Beratungsgespräch (zum Ergebnis Gesundheitsinterview grün/gelb/rot)", - "consultation_decision" to "Beratungsentscheidung (INTEGRATION_INDEX_POINTS)", + "consultation_decision" to "Beratungsentscheidung (RHS_POINTS)", "consent_conversation_in_6_months" to "Einverständnis Gespräch in 6 Monaten:", "participation_in_coaching" to "Teilnahme am Coaching", "decision_after_reflection_period" to "Entscheidung am .............. (Datum) nach Bedenkzeit", @@ -580,7 +580,7 @@ object LanguageManager { "select_one_answer_per_row" to "Please select one answer per row!", "no_next_question_defined" to "No forwarding page defined", "date_consultation_health_interview_result" to "Date of counseling interview (health interview result green/yellow/red)", - "consultation_decision" to "Counseling decision (INTEGRATION_INDEX_POINTS)", + "consultation_decision" to "Counseling decision (RHS_POINTS)", "consent_conversation_in_6_months" to "Consent for conversation in 6 months:", "participation_in_coaching" to "Participation in coaching", "decision_after_reflection_period" to "Decision on .............. (date) after reflection period", @@ -878,7 +878,7 @@ object LanguageManager { "select_one_answer_per_row" to "Veuillez sélectionner une réponse par ligne !", "no_next_question_defined" to "Aucune page de redirection définie", "date_consultation_health_interview_result" to "Date de l’entretien de conseil (résultat de l’entretien santé vert/jaune/rouge)", - "consultation_decision" to "Décision de conseil (INTEGRATION_INDEX_POINTS)", + "consultation_decision" to "Décision de conseil (RHS_POINTS)", "consent_conversation_in_6_months" to "Consentement pour entretien dans 6 mois :", "participation_in_coaching" to "Participation au coaching", "decision_after_reflection_period" to "Décision le .............. (date) après période de réflexion", @@ -1172,7 +1172,7 @@ object LanguageManager { "select_one_answer_per_row" to "Пожалуйста, выберите один ответ в каждой строке!", "no_next_question_defined" to "Следующая страница не определена", "date_consultation_health_interview_result" to "Дата консультации (результат медицинского интервью: зеленый/желтый/красный)", - "consultation_decision" to "Решение по консультации (INTEGRATION_INDEX_POINTS)", + "consultation_decision" to "Решение по консультации (RHS_POINTS)", "consent_conversation_in_6_months" to "Согласие на разговор через 6 месяцев:", "participation_in_coaching" to "Участие в коучинге", "decision_after_reflection_period" to "Решение от .............. (дата) после периода раздумий", @@ -1470,7 +1470,7 @@ object LanguageManager { "select_one_answer_per_row" to "Будь ласка, оберіть по одній відповіді в кожному рядку!", "no_next_question_defined" to "Наступна сторінка не визначена", "date_consultation_health_interview_result" to "Дата консультації (результат медичного інтерв’ю зелений/жовтий/червоний)", - "consultation_decision" to "Рішення консультації (INTEGRATION_INDEX_POINTS)", + "consultation_decision" to "Рішення консультації (RHS_POINTS)", "consent_conversation_in_6_months" to "Згода на розмову через 6 місяців:", "participation_in_coaching" to "Участь у коучингу", "decision_after_reflection_period" to "Рішення .............. (дата) після періоду роздумів", @@ -1768,7 +1768,7 @@ object LanguageManager { "select_one_answer_per_row" to "Lütfen her satır için bir cevap seçin!", "no_next_question_defined" to "Bir yönlendirme sayfası tanımlanmadı", "date_consultation_health_interview_result" to "Danışma görüşmesi tarihi (sağlık görüşmesi sonucu yeşil/sarı/kırmızı)", - "consultation_decision" to "Danışma kararı (INTEGRATION_INDEX_POINTS)", + "consultation_decision" to "Danışma kararı (RHS_POINTS)", "consent_conversation_in_6_months" to "6 ay içinde görüşme onayı:", "participation_in_coaching" to "Koçluğa katılım", "decision_after_reflection_period" to "Karar .............. (tarih) tarihinde düşünme süresinden sonra verildi", @@ -2066,7 +2066,7 @@ object LanguageManager { "select_one_answer_per_row" to "Proszę wybrać jedną odpowiedź w każdym wierszu!", "no_next_question_defined" to "Nie zdefiniowano strony przekierowania", "date_consultation_health_interview_result" to "Data rozmowy doradczej (wynik wywiadu zdrowotnego: zielony/żółty/czerwony)", - "consultation_decision" to "Decyzja doradcza (INTEGRATION_INDEX_POINTS)", + "consultation_decision" to "Decyzja doradcza (RHS_POINTS)", "consent_conversation_in_6_months" to "Zgoda na rozmowę za 6 miesięcy:", "participation_in_coaching" to "Udział w coachingu", "decision_after_reflection_period" to "Decyzja dnia .............. (data) po czasie do namysłu", @@ -2662,7 +2662,7 @@ object LanguageManager { "select_one_answer_per_row" to "Vă rugăm să selectați un răspuns pe rând!", "no_next_question_defined" to "Nu este definită o pagină de redirecționare", "date_consultation_health_interview_result" to "Data consilierii (rezultatul interviului de sănătate verde/galben/roșu)", - "consultation_decision" to "Decizia consilierii (INTEGRATION_INDEX_POINTS)", + "consultation_decision" to "Decizia consilierii (RHS_POINTS)", "consent_conversation_in_6_months" to "Consimțământ discuție peste 6 luni:", "participation_in_coaching" to "Participare la coaching", "decision_after_reflection_period" to "Decizie la .............. (dată) după perioada de reflecție", @@ -2960,7 +2960,7 @@ object LanguageManager { "select_one_answer_per_row" to "Por favor, seleccione una respuesta por fila.", "no_next_question_defined" to "No se definió ninguna pregunta de continuación", "date_consultation_health_interview_result" to "Fecha de la entrevista de orientación (sobre el resultado de la entrevista de salud: verde/amarillo/rojo)", - "consultation_decision" to "Decisión de orientación (INTEGRATION_INDEX_POINTS)", + "consultation_decision" to "Decisión de orientación (RHS_POINTS)", "consent_conversation_in_6_months" to "Consentimiento para entrevista en 6 meses:", "participation_in_coaching" to "Participación en el coaching", "decision_after_reflection_period" to "Decisión tomada el .............. (fecha) después del período de reflexión", diff --git a/app/src/main/java/com/dano/test1/LoginManager.kt b/app/src/main/java/com/dano/test1/LoginManager.kt new file mode 100644 index 0000000..34f8a0f --- /dev/null +++ b/app/src/main/java/com/dano/test1/LoginManager.kt @@ -0,0 +1,72 @@ +package com.dano.test1 + +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 okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +import org.json.JSONObject + +object LoginManager { + + private const val SERVER_LOGIN_URL = "http://49.13.157.44/login.php" + private val client = OkHttpClient() + + /** + * Startet den Login-Prozess. + * + * @param context Android Context + * @param password Vom User eingegebenes Passwort + * @param onSuccess Callback mit dem Token wenn Login erfolgreich + * @param onError Callback mit Fehlermeldung + */ + fun loginUser( + context: Context, + password: String, + onSuccess: (String) -> Unit, + onError: (String) -> Unit + ) { + CoroutineScope(Dispatchers.IO).launch { + try { + val requestBody = """{"password":"$password"}""" + .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.getBoolean("success")) { + val token = json.getString("token") + withContext(Dispatchers.Main) { + onSuccess(token) + } + } else { + withContext(Dispatchers.Main) { + onError("Login fehlgeschlagen") + } + } + } else { + withContext(Dispatchers.Main) { + onError("Fehler beim Login (${response.code})") + } + } + } catch (e: Exception) { + Log.e("LOGIN", "Exception beim Login", e) + withContext(Dispatchers.Main) { + onError("Exception: ${e.message}") + } + } + } + } +}