Compare commits
9 Commits
ac2e0dabd2
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| d30c94beeb | |||
| 5b1264293c | |||
| 39a4811fd2 | |||
| 8b3bb358e8 | |||
| 5968bf68d1 | |||
| ad09bce68c | |||
| 4089841336 | |||
| 5570710da5 | |||
| 8d54315fe7 |
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,4 +1,3 @@
|
||||
// app/src/main/java/com/dano/test1/AES256Helper.kt
|
||||
package com.dano.test1
|
||||
|
||||
import java.io.File
|
||||
@ -11,7 +10,6 @@ import kotlin.math.min
|
||||
|
||||
object AES256Helper {
|
||||
|
||||
// HKDF-SHA256: IKM = tokenHex->bytes, salt="", info="qdb-aes", len=32
|
||||
private fun hkdfFromToken(tokenHex: String, info: String = "qdb-aes", len: Int = 32): ByteArray {
|
||||
val ikm = hexToBytes(tokenHex)
|
||||
val mac = Mac.getInstance("HmacSHA256")
|
||||
|
||||
@ -3,6 +3,18 @@ package com.dano.test1.data
|
||||
import androidx.room.Database
|
||||
import androidx.room.RoomDatabase
|
||||
|
||||
/*
|
||||
Zentrale Room-Datenbank der App. Diese Klasse beschreibt:
|
||||
- welche Tabellen (entities) es gibt: Client, Questionnaire, Question, Answer, CompletedQuestionnaire
|
||||
- die Datenbank-Version (version = 1) für Migrations/Schema-Updates
|
||||
|
||||
Über die abstrakten DAO-Getter (clientDao(), questionnaireDao(), …) erhält der Rest der App Typsichere Zugriffe auf die jeweiligen Tabellen.
|
||||
|
||||
Hinweis:
|
||||
- Room erzeugt zur Build-Zeit die konkrete Implementierung dieser abstrakten Klasse.
|
||||
- Eine Instanz der Datenbank wird typischerweise per Room.databaseBuilder(...) erstellt und als Singleton verwendet.
|
||||
*/
|
||||
|
||||
@Database(
|
||||
entities = [
|
||||
Client::class,
|
||||
|
||||
@ -2,8 +2,18 @@ package com.dano.test1.data
|
||||
|
||||
import androidx.room.*
|
||||
|
||||
/*
|
||||
Data-Access-Objekte (DAOs) für die Room-Datenbank.
|
||||
Sie kapseln alle typsicheren Lese-/Schreiboperationen für die Tabellen clients, questionnaires, questions, answers und completed_questionnaires.
|
||||
|
||||
Hinweis:
|
||||
- Die konkreten Implementierungen erzeugt Room zur Build-Zeit.
|
||||
- DAOs werden über die AppDatabase (Room.databaseBuilder(...)) bezogen.
|
||||
*/
|
||||
|
||||
@Dao
|
||||
interface ClientDao {
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
suspend fun insertClient(client: Client)
|
||||
|
||||
@ -20,9 +30,9 @@ interface ClientDao {
|
||||
suspend fun getAllClients(): List<Client>
|
||||
}
|
||||
|
||||
|
||||
@Dao
|
||||
interface QuestionnaireDao {
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
suspend fun insertQuestionnaire(questionnaire: Questionnaire)
|
||||
|
||||
@ -30,11 +40,12 @@ interface QuestionnaireDao {
|
||||
suspend fun getById(id: String): Questionnaire?
|
||||
|
||||
@Query("SELECT * FROM questionnaires")
|
||||
suspend fun getAll(): List<Questionnaire> // <-- NEU
|
||||
suspend fun getAll(): List<Questionnaire>
|
||||
}
|
||||
|
||||
@Dao
|
||||
interface QuestionDao {
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
suspend fun insertQuestions(questions: List<Question>)
|
||||
|
||||
@ -48,8 +59,10 @@ interface QuestionDao {
|
||||
suspend fun getQuestionsForQuestionnaire(questionnaireId: String): List<Question>
|
||||
}
|
||||
|
||||
|
||||
@Dao
|
||||
interface AnswerDao {
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertAnswers(answers: List<Answer>)
|
||||
|
||||
@ -79,9 +92,9 @@ interface AnswerDao {
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@Dao
|
||||
interface CompletedQuestionnaireDao {
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insert(entry: CompletedQuestionnaire)
|
||||
|
||||
|
||||
@ -14,7 +14,7 @@ import java.io.FileOutputStream
|
||||
object DatabaseDownloader {
|
||||
|
||||
private const val DB_NAME = "questionnaire_database"
|
||||
private const val SERVER_DOWNLOAD_URL = "http://49.13.157.44/downloadFull.php"
|
||||
private const val SERVER_DOWNLOAD_URL = "https://daniel-ocks.de/qdb/downloadFull.php"
|
||||
|
||||
private val client = OkHttpClient()
|
||||
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
// app/src/main/java/com/dano/test1/DatabaseUploader.kt
|
||||
package com.dano.test1
|
||||
|
||||
import android.content.Context
|
||||
@ -21,25 +20,11 @@ import kotlin.system.exitProcess
|
||||
object DatabaseUploader {
|
||||
|
||||
private const val DB_NAME = "questionnaire_database"
|
||||
private const val SERVER_DELTA_URL = "http://49.13.157.44/uploadDeltaTest5.php"
|
||||
private const val SERVER_CHECK_URL = "http://49.13.157.44/checkDatabaseExists.php"
|
||||
private const val SERVER_DELTA_URL = "https://daniel-ocks.de/qdb/uploadDeltaTest5.php"
|
||||
private const val SERVER_CHECK_URL = "https://daniel-ocks.de/qdb/checkDatabaseExists.php"
|
||||
|
||||
private val client = OkHttpClient()
|
||||
|
||||
/** 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 (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 {
|
||||
@ -49,6 +34,7 @@ object DatabaseUploader {
|
||||
return@launch
|
||||
}
|
||||
|
||||
// WAL sauber schließen (falls aktiv)
|
||||
try {
|
||||
val db = SQLiteDatabase.openDatabase(dbFile.absolutePath, null, SQLiteDatabase.OPEN_READWRITE)
|
||||
db.rawQuery("PRAGMA wal_checkpoint(FULL);", null).use { /* noop */ }
|
||||
@ -82,11 +68,14 @@ object DatabaseUploader {
|
||||
put("questionnaires", queryToJsonArray(db, "SELECT id FROM questionnaires"))
|
||||
put("questions", queryToJsonArray(db, "SELECT questionId, questionnaireId, question FROM questions"))
|
||||
put("answers", queryToJsonArray(db, "SELECT clientCode, questionId, answerValue FROM answers"))
|
||||
put("completed_questionnaires",
|
||||
queryToJsonArray(db, "SELECT clientCode, questionnaireId, timestamp, isDone, sumPoints FROM completed_questionnaires"))
|
||||
put(
|
||||
"completed_questionnaires",
|
||||
queryToJsonArray(db, "SELECT clientCode, questionnaireId, timestamp, isDone, sumPoints FROM completed_questionnaires")
|
||||
)
|
||||
}
|
||||
db.close()
|
||||
|
||||
// JSON -> verschlüsselte Payload
|
||||
val tmpJson = File(context.cacheDir, "payload.json").apply { writeText(data.toString()) }
|
||||
val tmpEnc = File(context.cacheDir, "payload.enc")
|
||||
try {
|
||||
@ -98,11 +87,17 @@ object DatabaseUploader {
|
||||
|
||||
val body = MultipartBody.Builder()
|
||||
.setType(MultipartBody.FORM)
|
||||
.addFormDataPart("token", token)
|
||||
.addFormDataPart("token", token) // bleibt für Kompatibilität enthalten
|
||||
.addFormDataPart("file", "payload.enc", tmpEnc.asRequestBody("application/octet-stream".toMediaType()))
|
||||
.build()
|
||||
|
||||
val request = Request.Builder().url("http://49.13.157.44/uploadDeltaTest5.php").post(body).build()
|
||||
// WICHTIG: Jetzt HTTPS + Konstanten-URL verwenden, plus Bearer-Header
|
||||
val request = Request.Builder()
|
||||
.url(SERVER_DELTA_URL)
|
||||
.post(body)
|
||||
.header("Authorization", "Bearer $token")
|
||||
.build()
|
||||
|
||||
client.newCall(request).enqueue(object : Callback {
|
||||
override fun onFailure(call: Call, e: IOException) {
|
||||
Log.e("UPLOAD", "Fehlgeschlagen: ${e.message}")
|
||||
@ -113,7 +108,7 @@ object DatabaseUploader {
|
||||
if (response.isSuccessful) {
|
||||
Log.d("UPLOAD", "OK: $respBody")
|
||||
|
||||
// <<< alte Logik wieder aktivieren: lokale DB + Nebendateien löschen
|
||||
// alte Logik: lokale DB + Nebendateien löschen
|
||||
try {
|
||||
if (!file.delete()) Log.w("UPLOAD", "Lokale DB nicht gelöscht.")
|
||||
File(file.parent, "${file.name}-journal").delete()
|
||||
@ -122,11 +117,12 @@ object DatabaseUploader {
|
||||
} catch (e: Exception) {
|
||||
Log.w("UPLOAD", "Fehler beim Löschen lokaler DB-Dateien", e)
|
||||
}
|
||||
// >>>
|
||||
} else {
|
||||
Log.e("UPLOAD", "HTTP ${response.code}: $respBody")
|
||||
}
|
||||
tmpJson.delete(); tmpEnc.delete()
|
||||
|
||||
// unverändert beibehalten
|
||||
try { exitProcess(0) } catch (_: Exception) {}
|
||||
}
|
||||
})
|
||||
@ -162,6 +158,8 @@ object DatabaseUploader {
|
||||
}
|
||||
|
||||
fun uploadDatabaseWithToken(context: Context, token: String) {
|
||||
uploadDatabase(context, token) // nutzt die bestehende interne Logik
|
||||
uploadDatabase(context, token)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@ -2,16 +2,41 @@ package com.dano.test1.data
|
||||
|
||||
import androidx.room.*
|
||||
|
||||
/*
|
||||
Room-Entities (Tabellen) der App.
|
||||
- Definieren das Schema für Clients, Questionnaires, Questions, Answers und CompletedQuestionnaires.
|
||||
- Beziehungen:
|
||||
* Question -> Questionnaire (FK, CASCADE)
|
||||
* Answer -> Client (FK, CASCADE)
|
||||
* Answer -> Question (FK, CASCADE)
|
||||
* CompletedQuestionnaire -> Client (FK, CASCADE)
|
||||
* CompletedQuestionnaire -> Questionnaire (FK, CASCADE)
|
||||
- Primärschlüssel:
|
||||
* Client: clientCode
|
||||
* Questionnaire: id
|
||||
* Question: questionId
|
||||
* Answer: (clientCode, questionId) – eine Antwort je Client & Frage
|
||||
* CompletedQuestionnaire: (clientCode, questionnaireId) – ein Status je Client & Fragebogen
|
||||
*/
|
||||
|
||||
/* Tabelle: clients – Eindeutige Identifikation eines Clients per clientCode. */
|
||||
@Entity(tableName = "clients")
|
||||
data class Client(
|
||||
@PrimaryKey val clientCode: String,
|
||||
)
|
||||
|
||||
/* Tabelle: questionnaires – Eindeutige Fragebogen-IDs. */
|
||||
@Entity(tableName = "questionnaires")
|
||||
data class Questionnaire(
|
||||
@PrimaryKey val id: String,
|
||||
)
|
||||
|
||||
/*
|
||||
Tabelle: questions
|
||||
- Jede Frage gehört zu genau einem Fragebogen (questionnaireId).
|
||||
- Fremdschlüssel sorgt dafür, dass beim Löschen eines Fragebogens die zugehörigen Fragen mit gelöscht werden.
|
||||
- Index auf questionnaireId beschleunigt Abfragen „alle Fragen eines Fragebogens“.
|
||||
*/
|
||||
@Entity(
|
||||
tableName = "questions",
|
||||
foreignKeys = [
|
||||
@ -30,6 +55,12 @@ data class Question(
|
||||
val question: String = ""
|
||||
)
|
||||
|
||||
/*
|
||||
Tabelle: answers
|
||||
- Zusammengesetzter Primärschlüssel (clientCode, questionId):
|
||||
* Pro Client und Frage existiert höchstens eine Antwort.
|
||||
* Löscht man den Client oder die Frage, werden die zugehörigen Antworten mit entfernt.
|
||||
*/
|
||||
@Entity(
|
||||
tableName = "answers",
|
||||
primaryKeys = ["clientCode", "questionId"],
|
||||
@ -55,6 +86,17 @@ data class Answer(
|
||||
val answerValue: String = ""
|
||||
)
|
||||
|
||||
/*
|
||||
Tabelle: completed_questionnaires
|
||||
- Zusammengesetzter Primärschlüssel (clientCode, questionnaireId):
|
||||
* Hält den Abschluss-Status eines Fragebogens pro Client.
|
||||
- FKs mit CASCADE:
|
||||
* Beim Löschen eines Clients oder Fragebogens verschwindet der Status-Eintrag ebenfalls.
|
||||
- Indizes auf clientCode und questionnaireId für schnelle Lookups.
|
||||
- timestamp: Zeitpunkt der Statusänderung (Default: now).
|
||||
- isDone: true/false – abgeschlossen oder nicht.
|
||||
- sumPoints: optionaler Score des Fragebogens.
|
||||
*/
|
||||
@Entity(
|
||||
tableName = "completed_questionnaires",
|
||||
primaryKeys = ["clientCode", "questionnaireId"],
|
||||
@ -81,4 +123,3 @@ data class CompletedQuestionnaire(
|
||||
val isDone: Boolean,
|
||||
val sumPoints: Int? = null
|
||||
)
|
||||
|
||||
|
||||
@ -10,12 +10,29 @@ import android.provider.MediaStore
|
||||
import org.apache.poi.ss.usermodel.Row
|
||||
import org.apache.poi.xssf.usermodel.XSSFWorkbook
|
||||
|
||||
/*
|
||||
Aufgabe:
|
||||
- Baut eine Excel-Datei (XLSX) mit allen Clients als Zeilen und einem konfigurierbaren Spalten-Layout.
|
||||
- Speichert die Datei ausschließlich in den öffentlichen „Downloads“-Ordner
|
||||
|
||||
Datenquelle:
|
||||
- Liest die Spaltenreihenfolge/Spalten-IDs über HeaderOrderRepository.loadOrderedIds().
|
||||
- Holt alle Clients, Fragebogen-IDs sowie Antworten aus der lokalen Room-Datenbank
|
||||
|
||||
Ausgabeformat (Sheet „Headers“):
|
||||
- Zeile 1: Spalten-IDs (erste Zelle „#“ für laufende Nummer).
|
||||
- Zeile 2: Englische Beschriftung/Fragetext je Spalte (ermittelt via englishQuestionForId + LanguageManager).
|
||||
- Ab Zeile 3: Pro Client eine Datenzeile.
|
||||
* Für Spalten-ID „client_code“: der Client-Code.
|
||||
* Für Spalten-IDs, die einem Fragebogen entsprechen (Questionnaire-ID): „Done“/„Not Done“ (Abschlussstatus).
|
||||
* Für sonstige Spalten-IDs (Antwort-IDs): Antwortwert oder „None“, falls leer.
|
||||
*/
|
||||
class ExcelExportService(
|
||||
private val context: Context,
|
||||
private val headerRepo: HeaderOrderRepository
|
||||
) {
|
||||
|
||||
/** Baut die Excel-Datei und speichert sie ausschließlich unter "Downloads". */
|
||||
/* Baut die Excel-Datei und speichert sie ausschließlich unter "Downloads". */
|
||||
suspend fun exportHeadersForAllClients(): Uri? {
|
||||
val orderedIds = headerRepo.loadOrderedIds()
|
||||
if (orderedIds.isEmpty()) return null
|
||||
@ -78,7 +95,6 @@ class ExcelExportService(
|
||||
)
|
||||
}
|
||||
|
||||
/** Speichert Bytes nach "Downloads". */
|
||||
private fun saveToDownloads(filename: String, mimeType: String, bytes: ByteArray): Uri? {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
val resolver = context.contentResolver
|
||||
@ -108,7 +124,6 @@ class ExcelExportService(
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Export-spezifische Lokalisierung (EN) ----------
|
||||
private suspend fun englishQuestionForId(id: String, questionnaireIdSet: Set<String>): String {
|
||||
if (id == "client_code") return "Client code"
|
||||
if (id in questionnaireIdSet && !id.contains('-')) return "Questionnaire status"
|
||||
@ -147,7 +162,7 @@ class ExcelExportService(
|
||||
return stripped
|
||||
}
|
||||
|
||||
/** Englisch für Export; belässt Done/Not Done/None. */
|
||||
/* Englisch für Export; belässt Done/Not Done/None. */
|
||||
private fun localizeForExportEn(id: String, raw: String): String {
|
||||
if (id == "client_code") return raw
|
||||
if (raw == "Done" || raw == "Not Done" || raw == "None") return raw
|
||||
|
||||
@ -9,6 +9,11 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
/*
|
||||
Zweck :
|
||||
- Steuert die Eingabeseite für „Client Code“ und „Coach Code“ innerhalb des Fragebogen-Flows.
|
||||
*/
|
||||
|
||||
class HandlerClientCoachCode(
|
||||
private val answers: MutableMap<String, Any>,
|
||||
private val languageID: String,
|
||||
@ -48,7 +53,7 @@ class HandlerClientCoachCode(
|
||||
clientCodeField.isEnabled = true
|
||||
}
|
||||
|
||||
// === NEU: Coach-Code immer aus dem Login (TokenStore) setzen und sperren ===
|
||||
// Coach-Code immer aus dem Login (TokenStore) setzen und sperren
|
||||
val coachFromLogin = TokenStore.getUsername(layout.context)
|
||||
if (!coachFromLogin.isNullOrBlank()) {
|
||||
coachCodeField.setText(coachFromLogin)
|
||||
@ -122,7 +127,6 @@ class HandlerClientCoachCode(
|
||||
|
||||
override fun validate(): Boolean {
|
||||
val clientCode = layout.findViewById<EditText>(R.id.client_code).text
|
||||
// Validierung nimmt den (ggf. gesperrten) Text – passt
|
||||
val coachText = layout.findViewById<EditText>(R.id.coach_code).text
|
||||
return clientCode.isNotBlank() && coachText.isNotBlank()
|
||||
}
|
||||
@ -139,7 +143,6 @@ class HandlerClientCoachCode(
|
||||
// Not used
|
||||
}
|
||||
|
||||
// --- Helfer zum Sperren inkl. optischer Markierung (wie im Opening Screen) ---
|
||||
private fun lockCoachField(field: EditText) {
|
||||
field.isFocusable = false
|
||||
field.isFocusableInTouchMode = false
|
||||
|
||||
@ -3,6 +3,12 @@ package com.dano.test1
|
||||
import android.view.View
|
||||
import android.widget.*
|
||||
|
||||
/*
|
||||
Zweck:
|
||||
- Steuert die Seite „Client hat nicht unterschrieben“ im Fragebogenfluss.
|
||||
- Speichert den eingegebenen Coach-Code in das Answers-Map unter question.id (saveAnswer), damit der nachfolgende Prozess darauf zugreifen kann.
|
||||
*/
|
||||
|
||||
class HandlerClientNotSigned(
|
||||
private val answers: MutableMap<String, Any>,
|
||||
private val languageID: String,
|
||||
@ -14,7 +20,6 @@ class HandlerClientNotSigned(
|
||||
private lateinit var layout: View
|
||||
private lateinit var question: QuestionItem.ClientNotSigned
|
||||
|
||||
// UI components
|
||||
private lateinit var textView1: TextView
|
||||
private lateinit var textView2: TextView
|
||||
private lateinit var questionTextView: TextView
|
||||
@ -26,29 +31,24 @@ class HandlerClientNotSigned(
|
||||
this.layout = layout
|
||||
this.question = question
|
||||
|
||||
// Initialize UI components only once
|
||||
|
||||
initViews()
|
||||
|
||||
// Set localized text values from LanguageManager
|
||||
textView1.text = question.textKey1?.let { LanguageManager.getText(languageID, it) } ?: ""
|
||||
textView2.text = question.textKey2?.let { LanguageManager.getText(languageID, it) } ?: ""
|
||||
questionTextView.text = question.question?.let { LanguageManager.getText(languageID, it) } ?: ""
|
||||
|
||||
// Populate EditText with previous value if exists
|
||||
coachCodeField.setText(answers[question.id] as? String ?: "")
|
||||
|
||||
// Set click listener for Next button
|
||||
layout.findViewById<Button>(R.id.Qnext).setOnClickListener {
|
||||
onNextClicked()
|
||||
}
|
||||
|
||||
// Set click listener for Previous button
|
||||
layout.findViewById<Button>(R.id.Qprev).setOnClickListener {
|
||||
goToPreviousQuestion()
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize all views once to avoid repeated findViewById calls
|
||||
private fun initViews() {
|
||||
textView1 = layout.findViewById(R.id.textView1)
|
||||
textView2 = layout.findViewById(R.id.textView2)
|
||||
@ -56,7 +56,6 @@ class HandlerClientNotSigned(
|
||||
coachCodeField = layout.findViewById(R.id.coach_code)
|
||||
}
|
||||
|
||||
// Handle Next button click
|
||||
private fun onNextClicked() {
|
||||
if (validate()) {
|
||||
saveAnswer()
|
||||
@ -67,13 +66,11 @@ class HandlerClientNotSigned(
|
||||
}
|
||||
}
|
||||
|
||||
// Validate that coach code field is not empty
|
||||
override fun validate(): Boolean {
|
||||
val coachCode = coachCodeField.text
|
||||
return coachCode.isNotBlank()
|
||||
}
|
||||
|
||||
// Save entered coach code to answers map
|
||||
override fun saveAnswer() {
|
||||
answers[question.id] = coachCodeField.text.toString()
|
||||
}
|
||||
|
||||
@ -11,6 +11,11 @@ import android.util.TypedValue
|
||||
import androidx.core.widget.TextViewCompat
|
||||
import android.widget.AbsListView
|
||||
|
||||
/*
|
||||
Zweck:
|
||||
Rendert eine Datumsfrage mit drei Spinnern (Tag/Monat/Jahr) innerhalb des Fragebogen-Flows.
|
||||
*/
|
||||
|
||||
class HandlerDateSpinner(
|
||||
private val context: Context,
|
||||
private val answers: MutableMap<String, Any>,
|
||||
@ -46,13 +51,13 @@ class HandlerDateSpinner(
|
||||
questionTextView.text = question.question?.let { LanguageManager.getText(languageID, it) } ?: ""
|
||||
textView.text = question.textKey?.let { LanguageManager.getText(languageID, it) } ?: ""
|
||||
|
||||
// —— Schriftgrößen pro Bildschirmhöhe ——
|
||||
// Schriftgrößen pro Bildschirmhöhe
|
||||
setTextSizePercentOfScreenHeight(textView, 0.03f) // oben
|
||||
setTextSizePercentOfScreenHeight(questionTextView, 0.03f) // frage
|
||||
setTextSizePercentOfScreenHeight(labelDay, 0.025f)
|
||||
setTextSizePercentOfScreenHeight(labelMonth, 0.025f)
|
||||
setTextSizePercentOfScreenHeight(labelYear, 0.025f)
|
||||
// ————————————————————————————————
|
||||
//
|
||||
|
||||
// gespeicherte Antwort (YYYY-MM-DD) lesen
|
||||
val (savedYear, savedMonthIndex, savedDay) = question.question?.let {
|
||||
@ -202,7 +207,7 @@ class HandlerDateSpinner(
|
||||
return sdf.parse(dateString)
|
||||
}
|
||||
|
||||
// —— Textgröße prozentual zur Bildschirmhöhe (in sp) ——
|
||||
// Textgröße prozentual zur Bildschirmhöhe (in sp)
|
||||
private fun setTextSizePercentOfScreenHeight(view: TextView, percentOfHeight: Float) {
|
||||
val dm = (view.context ?: layout.context).resources.displayMetrics
|
||||
val sp = (dm.heightPixels * percentOfHeight) / dm.scaledDensity
|
||||
@ -210,7 +215,7 @@ class HandlerDateSpinner(
|
||||
view.setTextSize(TypedValue.COMPLEX_UNIT_SP, sp)
|
||||
}
|
||||
|
||||
// —— Spinner-Adapter: Schrift & Zeilenhöhe dynamisch, kein Abschneiden ——
|
||||
// Spinner-Adapter: Schrift & Zeilenhöhe dynamisch, kein Abschneiden
|
||||
private fun <T> setupSpinner(spinner: Spinner, items: List<T>, defaultSelection: T?) {
|
||||
val dm = context.resources.displayMetrics
|
||||
|
||||
@ -246,13 +251,13 @@ class HandlerDateSpinner(
|
||||
|
||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||
val v = super.getView(position, convertView, parent) as TextView
|
||||
styleRow(v, forceHeight = false) // ausgewählte Ansicht
|
||||
styleRow(v, forceHeight = false)
|
||||
return v
|
||||
}
|
||||
|
||||
override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||
val v = super.getDropDownView(position, convertView, parent) as TextView
|
||||
styleRow(v, forceHeight = true) // Dropdown-Zeilen: Höhe erzwingen
|
||||
styleRow(v, forceHeight = true)
|
||||
return v
|
||||
}
|
||||
}
|
||||
@ -260,7 +265,6 @@ class HandlerDateSpinner(
|
||||
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||
spinner.adapter = adapter
|
||||
|
||||
// Spinner selbst ausreichend hoch
|
||||
spinner.setPadding(spinner.paddingLeft, vPadPx, spinner.paddingRight, vPadPx)
|
||||
spinner.minimumHeight = rowHeight
|
||||
spinner.requestLayout()
|
||||
|
||||
@ -8,6 +8,14 @@ import android.widget.*
|
||||
import androidx.core.widget.TextViewCompat
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
/*
|
||||
|
||||
Zweck:
|
||||
- Stellt eine „Glas-Skala“-Frage dar, bei der pro Symptom (Zeile) genau eine von fünf Antwortstufen gewählt wird: never / little / moderate / much / extreme.
|
||||
- Die Stufen werden sowohl als RadioButtons als auch über eine feste Icon-Leiste visualisiert.
|
||||
|
||||
*/
|
||||
|
||||
class HandlerGlassScaleQuestion(
|
||||
private val context: Context,
|
||||
private val answers: MutableMap<String, Any>,
|
||||
@ -60,7 +68,7 @@ class HandlerGlassScaleQuestion(
|
||||
setTextSizePercentOfScreenHeight(titleTv, 0.03f)
|
||||
setTextSizePercentOfScreenHeight(questionTv, 0.03f)
|
||||
|
||||
// ----- feste Icon-Leiste -----
|
||||
// feste Icon-Leiste
|
||||
val header = layout.findViewById<LinearLayout>(R.id.glass_header)
|
||||
header.removeAllViews()
|
||||
header.addView(Space(context).apply {
|
||||
@ -80,7 +88,7 @@ class HandlerGlassScaleQuestion(
|
||||
cell.addView(img)
|
||||
header.addView(cell)
|
||||
}
|
||||
// -----------------------------
|
||||
//
|
||||
|
||||
val tableLayout = layout.findViewById<TableLayout>(R.id.glass_table)
|
||||
tableLayout.removeAllViews()
|
||||
@ -154,21 +162,26 @@ class HandlerGlassScaleQuestion(
|
||||
val radioGroup = RadioGroup(context).apply {
|
||||
orientation = RadioGroup.HORIZONTAL
|
||||
layoutParams = TableRow.LayoutParams(0, TableRow.LayoutParams.WRAP_CONTENT, 5f)
|
||||
setPadding(0, 0, 0, 0)
|
||||
}
|
||||
|
||||
// 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 rb = RadioButton(context).apply {
|
||||
tag = labelKey
|
||||
id = View.generateViewId()
|
||||
isChecked = savedLabel == labelKey
|
||||
setPadding(0, 0, 0, 0)
|
||||
}
|
||||
val lp = RadioGroup.LayoutParams(
|
||||
0, RadioGroup.LayoutParams.WRAP_CONTENT, 1f
|
||||
).apply { gravity = Gravity.CENTER }
|
||||
rb.layoutParams = lp
|
||||
radioGroup.addView(rb)
|
||||
rb.layoutParams = FrameLayout.LayoutParams(
|
||||
FrameLayout.LayoutParams.WRAP_CONTENT,
|
||||
FrameLayout.LayoutParams.WRAP_CONTENT,
|
||||
Gravity.CENTER
|
||||
)
|
||||
cell.addView(rb)
|
||||
radioGroup.addView(cell)
|
||||
}
|
||||
|
||||
row.addView(radioGroup)
|
||||
@ -176,6 +189,7 @@ class HandlerGlassScaleQuestion(
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun validate(): Boolean {
|
||||
val table = layout.findViewById<TableLayout>(R.id.glass_table)
|
||||
for (i in 0 until table.childCount) {
|
||||
@ -215,7 +229,6 @@ class HandlerGlassScaleQuestion(
|
||||
}
|
||||
}
|
||||
|
||||
// --- Helpers ---
|
||||
private fun getRadioFromChild(child: View): RadioButton? =
|
||||
when (child) {
|
||||
is RadioButton -> child
|
||||
|
||||
@ -9,6 +9,21 @@ import android.widget.TextView
|
||||
import androidx.core.widget.TextViewCompat
|
||||
import com.google.android.material.button.MaterialButton
|
||||
|
||||
/*
|
||||
Zweck:
|
||||
- Steuert die letzte Seite eines Fragebogens.
|
||||
- Zeigt Abschlusstexte an, speichert alle gesammelten Antworten in die lokale DB und beendet anschließend den Fragebogen und kehrt zur übergeordneten Ansicht zurück.
|
||||
|
||||
Beim Klick auf „Speichern“:
|
||||
- Ladezustand anzeigen (ProgressBar), Buttons deaktivieren.
|
||||
- Antworten asynchron in Room-DB persistieren (über `saveAnswersToDatabase`).
|
||||
- Punktsumme ermitteln und in `GlobalValues.INTEGRATION_INDEX` schreiben.
|
||||
- `client_code` (falls vorhanden) als `GlobalValues.LAST_CLIENT_CODE` merken.
|
||||
- Mindestens 2 Sekunden „Loading“-Dauer sicherstellen (ruhiges UX).
|
||||
- Zurück auf den Main-Thread wechseln, UI entsperren und Fragebogen schließen.
|
||||
|
||||
*/
|
||||
|
||||
class HandlerLastPage(
|
||||
private val answers: Map<String, Any>,
|
||||
private val languageID: String,
|
||||
@ -19,7 +34,7 @@ class HandlerLastPage(
|
||||
|
||||
private lateinit var currentQuestion: QuestionItem.LastPage
|
||||
private lateinit var layout: View
|
||||
private val minLoadingTimeMs = 2000L // Minimum loading time in milliseconds (2 seconds)
|
||||
private val minLoadingTimeMs = 2000L
|
||||
|
||||
override fun bind(layout: View, question: QuestionItem) {
|
||||
this.layout = layout
|
||||
@ -61,27 +76,31 @@ class HandlerLastPage(
|
||||
// Punkte summieren
|
||||
GlobalValues.INTEGRATION_INDEX = sumPoints()
|
||||
|
||||
// Client-Code merken
|
||||
// Client-Code merken (für Auto-Laden im Opening Screen)
|
||||
val clientCode = answers["client_code"] as? String
|
||||
if (clientCode != null) GlobalValues.LAST_CLIENT_CODE = clientCode
|
||||
if (clientCode != null) {
|
||||
GlobalValues.LAST_CLIENT_CODE = clientCode
|
||||
GlobalValues.LOADED_CLIENT_CODE = clientCode // <— zusätzlich setzen
|
||||
}
|
||||
|
||||
// min. Ladezeit einhalten
|
||||
// min. Ladezeit einhalten (ruhiges UX)
|
||||
val elapsedTime = System.currentTimeMillis() - startTime
|
||||
if (elapsedTime < minLoadingTimeMs) delay(minLoadingTimeMs - elapsedTime)
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
showLoading(false)
|
||||
val activity = layout.context as? MainActivity
|
||||
// Zurück zum Opening Screen – der lädt dann automatisch (siehe Änderung 2)
|
||||
activity?.finishQuestionnaire() ?: goToNextQuestion()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun validate(): Boolean = true
|
||||
override fun saveAnswer() {}
|
||||
|
||||
// ---------- Responsive Textgröße für den Finish-Button ----------
|
||||
private fun applyResponsiveTextSizing(btn: MaterialButton) {
|
||||
// Max-/Min-Sp anhand der Bildschirmhöhe (in sp) berechnen
|
||||
val dm = btn.resources.displayMetrics
|
||||
|
||||
@ -7,6 +7,11 @@ import kotlinx.coroutines.*
|
||||
import android.util.TypedValue
|
||||
import androidx.core.widget.TextViewCompat
|
||||
|
||||
/*
|
||||
Zweck:
|
||||
- Steuert eine Frage mit mehreren auswählbaren Antwortoptionen (Checkboxen).
|
||||
*/
|
||||
|
||||
class HandlerMultiCheckboxQuestion(
|
||||
private val context: Context,
|
||||
private val answers: MutableMap<String, Any>,
|
||||
@ -15,7 +20,7 @@ class HandlerMultiCheckboxQuestion(
|
||||
private val goToNextQuestion: () -> Unit,
|
||||
private val goToPreviousQuestion: () -> Unit,
|
||||
private val showToast: (String) -> Unit,
|
||||
private val questionnaireMeta: String // neu: für DB-ID wie bei den anderen Handlern
|
||||
private val questionnaireMeta: String //
|
||||
) : QuestionHandler {
|
||||
|
||||
private lateinit var layout: View
|
||||
@ -29,14 +34,12 @@ class HandlerMultiCheckboxQuestion(
|
||||
val questionTitle = layout.findViewById<TextView>(R.id.question)
|
||||
val questionTextView = layout.findViewById<TextView>(R.id.textView)
|
||||
|
||||
// Texte setzen
|
||||
questionTextView.text = this.question.textKey?.let { LanguageManager.getText(languageID, it) } ?: ""
|
||||
questionTitle.text = this.question.question?.let { LanguageManager.getText(languageID, it) } ?: ""
|
||||
|
||||
// ===== Textgrößen pro Bildschirmhöhe (wie bei deinen anderen Handlern) =====
|
||||
// Textgrößen pro Bildschirmhöhe (wie bei deinen anderen Handlern)
|
||||
setTextSizePercentOfScreenHeight(questionTextView, 0.03f) // Überschrift
|
||||
setTextSizePercentOfScreenHeight(questionTitle, 0.03f) // Frage
|
||||
// ==========================================================================
|
||||
|
||||
container.removeAllViews()
|
||||
|
||||
@ -45,13 +48,12 @@ class HandlerMultiCheckboxQuestion(
|
||||
(answers[it] as? List<*>)?.map { it.toString() }?.toSet()
|
||||
} ?: emptySet()
|
||||
|
||||
// ——— Checkbox-Schrift & Zeilenhöhe dynamisch ableiten (kein Abschneiden) ———
|
||||
// Checkbox-Schrift & Zeilenhöhe dynamisch ableiten (kein Abschneiden)
|
||||
val dm = layout.resources.displayMetrics
|
||||
val cbTextSp = (dm.heightPixels * 0.025f) / dm.scaledDensity // ~2.5% der Bildschirmhöhe
|
||||
val cbTextPx = cbTextSp * dm.scaledDensity
|
||||
val cbPadV = (cbTextPx * 0.40f).toInt()
|
||||
val cbMinH = (cbTextPx * 1.60f + 2 * cbPadV).toInt()
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
this.question.options.forEach { option ->
|
||||
val checkBox = CheckBox(context).apply {
|
||||
@ -78,7 +80,7 @@ class HandlerMultiCheckboxQuestion(
|
||||
container.addView(checkBox)
|
||||
}
|
||||
|
||||
// --- DB-Abfrage falls noch kein Eintrag im answers-Map existiert ---
|
||||
//DB-Abfrage falls noch kein Eintrag im answers-Map existiert
|
||||
val answerMapKey = question.question ?: (question.id ?: "")
|
||||
if (answerMapKey.isNotBlank() && !answers.containsKey(answerMapKey)) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
@ -100,7 +102,7 @@ class HandlerMultiCheckboxQuestion(
|
||||
cb.isChecked = parsed.contains(cb.tag.toString())
|
||||
}
|
||||
|
||||
// answers-Map aktualisieren (Liste)
|
||||
// answers-Map aktualisieren
|
||||
answers[answerMapKey] = parsed.toList()
|
||||
|
||||
// Punkte berechnen und hinzufügen
|
||||
@ -175,16 +177,8 @@ class HandlerMultiCheckboxQuestion(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parsen der DB-Antwort in ein Set von Keys. Unterstützt:
|
||||
* - JSON-Array: ["a","b"]
|
||||
* - kommasepariert: a,b
|
||||
* - semikolon-separiert: a;b
|
||||
* - einzelner Wert: a
|
||||
*/
|
||||
private fun parseMultiAnswer(dbAnswer: String): Set<String> {
|
||||
val trimmed = dbAnswer.trim()
|
||||
// JSON-Array-like
|
||||
if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
|
||||
val inner = trimmed.substring(1, trimmed.length - 1)
|
||||
if (inner.isBlank()) return emptySet()
|
||||
@ -194,7 +188,6 @@ class HandlerMultiCheckboxQuestion(
|
||||
.toSet()
|
||||
}
|
||||
|
||||
// If contains comma or semicolon
|
||||
val separator = when {
|
||||
trimmed.contains(",") -> ","
|
||||
trimmed.contains(";") -> ";"
|
||||
@ -211,7 +204,6 @@ class HandlerMultiCheckboxQuestion(
|
||||
}
|
||||
}
|
||||
|
||||
// Helper: Textgröße prozentual zur Bildschirmhöhe setzen
|
||||
private fun setTextSizePercentOfScreenHeight(view: TextView, percentOfHeight: Float) {
|
||||
val dm = (view.context ?: layout.context).resources.displayMetrics
|
||||
val sp = (dm.heightPixels * percentOfHeight) / dm.scaledDensity
|
||||
|
||||
@ -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)
|
||||
@ -255,18 +272,21 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
|
||||
}
|
||||
|
||||
private fun restorePreviousClientCode() {
|
||||
// Coach-Code (Username) setzen und Feld sperren – aber NICHT mehr zurückkehren
|
||||
val username = TokenStore.getUsername(activity)
|
||||
if (!username.isNullOrBlank()) {
|
||||
coachEditText.setText(username)
|
||||
lockCoachCodeField()
|
||||
return
|
||||
}
|
||||
|
||||
// Hier den zuletzt verwendeten Client-Code einsetzen
|
||||
GlobalValues.LAST_CLIENT_CODE?.let {
|
||||
editText.setText(it)
|
||||
GlobalValues.LOADED_CLIENT_CODE = it
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun setupLanguageSpinner() {
|
||||
val languages = listOf("GERMAN", "ENGLISH", "FRENCH", "ROMANIAN", "ARABIC", "POLISH", "TURKISH", "UKRAINIAN", "RUSSIAN", "SPANISH")
|
||||
val adapter = ArrayAdapter(activity, android.R.layout.simple_spinner_item, languages).apply {
|
||||
@ -441,13 +461,47 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
|
||||
private fun setupUploadButton() {
|
||||
uploadButton.text = t("upload")
|
||||
uploadButton.setOnClickListener {
|
||||
val token = TokenStore.getToken(activity)
|
||||
if (token.isNullOrBlank()) {
|
||||
Toast.makeText(activity, t("login_required"), Toast.LENGTH_LONG).show()
|
||||
return@setOnClickListener
|
||||
}
|
||||
// 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) {
|
||||
GlobalValues.LAST_CLIENT_CODE = editText.text.toString().trim()
|
||||
DatabaseUploader.uploadDatabaseWithToken(activity, token)
|
||||
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()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -482,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) {
|
||||
@ -579,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() {
|
||||
@ -604,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()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@ -8,6 +8,11 @@ import kotlinx.coroutines.*
|
||||
import android.util.TypedValue
|
||||
import androidx.core.widget.TextViewCompat // <— hinzugefügt
|
||||
|
||||
/*
|
||||
Zweck:
|
||||
- Steuert eine Einzelfrage mit genau einer auswählbaren Antwort (RadioButtons).
|
||||
*/
|
||||
|
||||
class HandlerRadioQuestion(
|
||||
private val context: Context,
|
||||
private val answers: MutableMap<String, Any>,
|
||||
@ -36,7 +41,7 @@ class HandlerRadioQuestion(
|
||||
Html.fromHtml(LanguageManager.getText(languageID, it), Html.FROM_HTML_MODE_LEGACY)
|
||||
} ?: ""
|
||||
|
||||
// === Schriftgrößen wie im HandlerClientCoachCode ===
|
||||
//
|
||||
// Titel/Frage: 3% der Bildschirmhöhe
|
||||
setTextSizePercentOfScreenHeight(questionTextView, 0.03f)
|
||||
setTextSizePercentOfScreenHeight(questionTitle, 0.03f)
|
||||
@ -132,7 +137,7 @@ class HandlerRadioQuestion(
|
||||
}
|
||||
}
|
||||
|
||||
// ——— Helper: setzt Textgröße prozentual zur Bildschirmhöhe (in sp) ———
|
||||
// setzt Textgröße prozentual zur Bildschirmhöhe (in sp)
|
||||
private fun setTextSizePercentOfScreenHeight(view: TextView, percentOfHeight: Float) {
|
||||
val dm = (view.context ?: layout.context).resources.displayMetrics
|
||||
val sp = (dm.heightPixels * percentOfHeight) / dm.scaledDensity
|
||||
|
||||
@ -9,6 +9,12 @@ import android.util.TypedValue
|
||||
import android.widget.TextView
|
||||
import androidx.core.widget.TextViewCompat
|
||||
|
||||
/*
|
||||
Zweck:
|
||||
- Steuert eine Frage mit einer einzelnen Auswahl aus einer Dropdown-Liste (Spinner).
|
||||
- Baut die Optionen dynamisch auf, lokalisiert Texte, stellt responsive Typografie her und kann vorhandene Antworten aus der lokalen Room-DB restaurieren.
|
||||
*/
|
||||
|
||||
class HandlerStringSpinner(
|
||||
private val context: Context,
|
||||
private val answers: MutableMap<String, Any>,
|
||||
@ -16,7 +22,7 @@ class HandlerStringSpinner(
|
||||
private val goToNextQuestion: () -> Unit,
|
||||
private val goToPreviousQuestion: () -> Unit,
|
||||
private val showToast: (String) -> Unit,
|
||||
private val questionnaireMeta: String // Meta, damit dieselbe DB-ID wie in anderen Handlern gebildet wird
|
||||
private val questionnaireMeta: String
|
||||
) : QuestionHandler {
|
||||
|
||||
private lateinit var layout: View
|
||||
@ -36,20 +42,19 @@ class HandlerStringSpinner(
|
||||
questionTextView.text = question.question?.let { LanguageManager.getText(languageID, it) } ?: ""
|
||||
textView.text = question.textKey?.let { LanguageManager.getText(languageID, it) } ?: ""
|
||||
|
||||
// === Textgrößen prozentual zur Bildschirmhöhe (wie im HandlerRadioQuestion) ===
|
||||
// Textgrößen prozentual zur Bildschirmhöhe (wie im HandlerRadioQuestion)
|
||||
setTextSizePercentOfScreenHeight(textView, 0.03f)
|
||||
setTextSizePercentOfScreenHeight(questionTextView, 0.03f)
|
||||
// ==============================================================================
|
||||
|
||||
val options = buildOptionsList()
|
||||
|
||||
// vorhandene Auswahl (falls vorhanden)
|
||||
val savedSelection = question.question?.let { answers[it] as? String }
|
||||
|
||||
// Spinner aufsetzen (Schriftgröße & Zeilenhöhe dynamisch, kein Abschneiden)
|
||||
// Spinner aufsetzen
|
||||
setupSpinner(spinner, options, savedSelection)
|
||||
|
||||
// Falls noch keine Antwort im Map: aus DB laden (analog zu anderen Handlern)
|
||||
// Falls noch keine Antwort im Map: aus DB laden
|
||||
val answerMapKey = question.question ?: (question.id ?: "")
|
||||
if (answerMapKey.isNotBlank() && !answers.containsKey(answerMapKey)) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
|
||||
@ -8,6 +8,14 @@ import kotlinx.coroutines.*
|
||||
import android.util.TypedValue
|
||||
import androidx.core.widget.TextViewCompat // <- NEU
|
||||
|
||||
/*
|
||||
Zweck:
|
||||
- Steuert eine Frage, bei der ein numerischer Wert aus einem Spinner gewählt wird.
|
||||
- Unterstützt sowohl feste Optionslisten als auch numerische Bereiche (min..max).
|
||||
- Lokalisiert Texte, kann eine frühere Antwort aus der lokalen Room-DB (per AnswerDao) wiederherstellen.
|
||||
|
||||
*/
|
||||
|
||||
class HandlerValueSpinner(
|
||||
private val context: Context,
|
||||
private val answers: MutableMap<String, Any>,
|
||||
@ -16,7 +24,7 @@ class HandlerValueSpinner(
|
||||
private val goToPreviousQuestion: () -> Unit,
|
||||
private val goToQuestionById: (String) -> Unit,
|
||||
private val showToast: (String) -> Unit,
|
||||
private val questionnaireMeta: String // neu: für die DB-Abfrage
|
||||
private val questionnaireMeta: String
|
||||
) : QuestionHandler {
|
||||
|
||||
private lateinit var layout: View
|
||||
@ -35,11 +43,10 @@ class HandlerValueSpinner(
|
||||
questionTextView.text = question.question?.let { LanguageManager.getText(languageID, it) } ?: ""
|
||||
textView.text = question.textKey?.let { LanguageManager.getText(languageID, it) } ?: ""
|
||||
|
||||
// === Schriftgrößen wie im HandlerRadioQuestion ===
|
||||
// Schriftgrößen wie im HandlerRadioQuestion
|
||||
// Titel/Frage: 3% der Bildschirmhöhe
|
||||
setTextSizePercentOfScreenHeight(textView, 0.03f)
|
||||
setTextSizePercentOfScreenHeight(questionTextView, 0.03f)
|
||||
// =================================================
|
||||
|
||||
val prompt = LanguageManager.getText(languageID, "choose_answer")
|
||||
val spinnerItems: List<String> = listOf(prompt) + if (question.range != null) {
|
||||
@ -51,7 +58,7 @@ class HandlerValueSpinner(
|
||||
val savedValue = question.question?.let { answers[it] as? String }
|
||||
setupSpinner(spinner, spinnerItems, savedValue)
|
||||
|
||||
// --- DB-Abfrage falls noch keine Antwort im Map existiert ---
|
||||
//DB-Abfrage falls noch keine Antwort im Map existiert
|
||||
val answerMapKey = question.question ?: (question.id ?: "")
|
||||
if (answerMapKey.isNotBlank() && !answers.containsKey(answerMapKey)) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
@ -120,14 +127,13 @@ class HandlerValueSpinner(
|
||||
}
|
||||
}
|
||||
|
||||
// ——— Helper: setzt Textgröße prozentual zur Bildschirmhöhe (in sp) ———
|
||||
// setzt Textgröße prozentual zur Bildschirmhöhe (in sp)
|
||||
private fun setTextSizePercentOfScreenHeight(view: TextView, percentOfHeight: Float) {
|
||||
val dm = (view.context ?: layout.context).resources.displayMetrics
|
||||
val sp = (dm.heightPixels * percentOfHeight) / dm.scaledDensity
|
||||
TextViewCompat.setAutoSizeTextTypeWithDefaults(view, TextViewCompat.AUTO_SIZE_TEXT_TYPE_NONE)
|
||||
view.setTextSize(TypedValue.COMPLEX_UNIT_SP, sp)
|
||||
}
|
||||
// ————————————————————————————————————————————————————————————————
|
||||
|
||||
private fun <T> setupSpinner(spinner: Spinner, items: List<T>, selectedItem: T?) {
|
||||
val dm = context.resources.displayMetrics
|
||||
|
||||
@ -7,6 +7,12 @@ import org.apache.poi.xssf.usermodel.XSSFWorkbook
|
||||
import org.json.JSONArray
|
||||
import java.nio.charset.Charset
|
||||
|
||||
/*
|
||||
Zweck:
|
||||
- Liefert die Reihenfolge/IDs der zu exportierenden Header (Spalten) für den Excel-Export.
|
||||
- Bevorzugte Quelle ist eine Excel-Datei aus den App-Assets („header_order.xlsx“), als Fallback wird eine JSON-Datei („header_order.json“) genutzt.
|
||||
|
||||
*/
|
||||
class HeaderOrderRepository(
|
||||
private val context: Context,
|
||||
// Sprache abrufen (Standard: Deutsch, damit es ohne OpeningScreen schon sinnvoll ist)
|
||||
|
||||
@ -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?"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@ -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
|
||||
@ -13,7 +15,8 @@ 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 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) {
|
||||
if (!response.isSuccessful || text == null) {
|
||||
withContext(Dispatchers.Main) { onError("Fehler beim Login (${response.code})") }
|
||||
return@launch
|
||||
}
|
||||
|
||||
val json = JSONObject(text)
|
||||
if (json.optBoolean("success")) {
|
||||
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")
|
||||
// => setzt auch den Login-Timestamp:
|
||||
TokenStore.save(context, token, username)
|
||||
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", 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}") }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,6 +6,19 @@ import androidx.room.Room
|
||||
import androidx.room.RoomDatabase
|
||||
import com.dano.test1.data.AppDatabase
|
||||
|
||||
/*
|
||||
MyApp (Application)
|
||||
- Einstiegspunkt der App, der einmal pro Prozessstart initialisiert wird.
|
||||
|
||||
Besonderheiten der DB-Konfiguration:
|
||||
- Name: "questionnaire_database"
|
||||
- fallbackToDestructiveMigration():
|
||||
* Falls sich das Schema ändert und keine Migration vorliegt,wird die DB zerstört und neu angelegt.
|
||||
- setJournalMode(TRUNCATE):
|
||||
* Verwendet TRUNCATE-Journal (keine separaten -wal/-shm Dateien), es existiert nur die Hauptdatei „questionnaire_database“.
|
||||
- Callback onOpen():
|
||||
* Loggt beim Öffnen der Datenbank einen Hinweis.
|
||||
*/
|
||||
class MyApp : Application() {
|
||||
|
||||
companion object {
|
||||
@ -16,7 +29,7 @@ class MyApp : Application() {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
// Room Datenbank bauen: nur die Hauptdatei, ohne WAL und Journal
|
||||
// Room-Datenbank bauen: nur die Hauptdatei, ohne WAL und Journal
|
||||
database = Room.databaseBuilder(
|
||||
applicationContext,
|
||||
AppDatabase::class.java,
|
||||
|
||||
@ -4,6 +4,20 @@ import android.content.Context
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.NetworkCapabilities
|
||||
|
||||
/*
|
||||
Zweck:
|
||||
- Einfache Hilfsklasse, um den aktuellen Online-Status des Geräts zu prüfen.
|
||||
|
||||
Funktionsweise:
|
||||
- `isOnline(context)` nutzt den systemweiten `ConnectivityManager`, fragt die aktive Verbindung (`activeNetwork`) ab und prüft deren `NetworkCapabilities`.
|
||||
- Es wird nur dann `true` zurückgegeben, wenn:
|
||||
* eine aktive Verbindung existiert und
|
||||
* die Verbindung die Fähigkeit „INTERNET“ besitzt und
|
||||
* die Verbindung als „VALIDATED“ gilt (vom System als funktionsfähig verifiziert).
|
||||
|
||||
Verwendung:
|
||||
- Vo Netzwerkaufrufen (Login, Upload, Download) aufrufen, um „Offline“-Fälle frühzeitig abzufangen und nutzerfreundliche Meldungen zu zeigen.
|
||||
*/
|
||||
object NetworkUtils {
|
||||
fun isOnline(context: Context): Boolean {
|
||||
return try {
|
||||
|
||||
@ -2,20 +2,32 @@ package com.dano.test1
|
||||
|
||||
import android.content.Context
|
||||
|
||||
/*
|
||||
TokenStore
|
||||
- Kleiner Helper zum Verwalten der Login-Sitzung
|
||||
- Speichert:
|
||||
* das API-Token (KEY_TOKEN),
|
||||
* den Usernamen (KEY_USER),
|
||||
* den Zeitpunkt des Logins in Millisekunden (KEY_LOGIN_TS).
|
||||
- Bietet Lese-/Schreib-Methoden sowie ein clear(), um alles zurückzusetzen.
|
||||
|
||||
Hinweis:
|
||||
- Die Daten liegen in einer privaten App-Preference-Datei (PREF = "qdb_prefs").
|
||||
- getLoginTimestamp() eignet sich, um die Token-„Frische“ (Alter) zu berechnen.
|
||||
*/
|
||||
object TokenStore {
|
||||
private const val PREF = "qdb_prefs"
|
||||
private const val KEY_TOKEN = "token"
|
||||
private const val KEY_USER = "user"
|
||||
private const val KEY_LOGIN_TS = "login_ts"
|
||||
|
||||
/** Speichert Token, Username und Login-Timestamp (jetzt) */
|
||||
fun save(context: Context, token: String, username: String) {
|
||||
val now = System.currentTimeMillis()
|
||||
context.getSharedPreferences(PREF, Context.MODE_PRIVATE)
|
||||
.edit()
|
||||
.putString(KEY_TOKEN, token)
|
||||
.putString(KEY_USER, username)
|
||||
.putLong(KEY_LOGIN_TS, now)
|
||||
.putString(KEY_TOKEN, token) // API-/Session-Token
|
||||
.putString(KEY_USER, username) // angemeldeter Benutzername
|
||||
.putLong(KEY_LOGIN_TS, now) // Zeitpunkt des Logins
|
||||
.apply()
|
||||
}
|
||||
|
||||
|
||||
@ -5,9 +5,14 @@
|
||||
android:id="@+id/main"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".MainActivity"
|
||||
tools:layout_editor_absoluteX="11dp"
|
||||
tools:layout_editor_absoluteY="107dp">
|
||||
tools:context=".MainActivity">
|
||||
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
android:id="@+id/gTop"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
app:layout_constraintGuide_begin="32dp" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/Qprev"
|
||||
@ -27,7 +32,6 @@
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
<!-- Weiter -->
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/Qnext"
|
||||
android:layout_width="@dimen/nav_btn_size"
|
||||
@ -46,6 +50,34 @@
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:textStyle="bold"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/gTop"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintWidth_percent="0.9" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/question"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:textStyle="bold"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="8dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/textView"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintWidth_percent="0.9" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/client_code"
|
||||
android:layout_width="0dp"
|
||||
@ -54,7 +86,7 @@
|
||||
app:layout_constraintHeight_percent="0.08"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/question"
|
||||
app:layout_constraintTop_toBottomOf="@id/question"
|
||||
android:layout_marginTop="16dp"
|
||||
android:background="@android:drawable/edit_text"
|
||||
android:ems="10"
|
||||
@ -90,34 +122,4 @@
|
||||
android:autoSizeMaxTextSize="36sp"
|
||||
android:autoSizeStepGranularity="2sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:gravity="center"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHeight_percent="0.15"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_bias="0.051"
|
||||
app:layout_constraintWidth_percent="0.9" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/question"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:gravity="center"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHeight_percent="0.15"
|
||||
app:layout_constraintHorizontal_bias="0.512"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/textView"
|
||||
app:layout_constraintVertical_bias="0.0"
|
||||
app:layout_constraintWidth_percent="0.9" />
|
||||
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@ -1,39 +1,42 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<!-- Obere Überschrift -->
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
android:id="@+id/gTop"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
app:layout_constraintGuide_begin="32dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHeight_percent="0.15"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/gTop"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_bias="0.051"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintWidth_percent="0.9" />
|
||||
|
||||
<!-- Frage -->
|
||||
<TextView
|
||||
android:id="@+id/question"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHeight_percent="0.15"
|
||||
app:layout_constraintHorizontal_bias="0.512"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="8dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/textView"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/textView"
|
||||
app:layout_constraintVertical_bias="0.0"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintWidth_percent="0.9" />
|
||||
|
||||
<TextView
|
||||
@ -129,7 +132,6 @@
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
<!-- Weiter -->
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/Qnext"
|
||||
android:layout_width="@dimen/nav_btn_size"
|
||||
|
||||
@ -1,41 +1,44 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<!-- Titel -->
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
android:id="@+id/gTop"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
app:layout_constraintGuide_begin="32dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHeight_percent="0.15"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/gTop"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_bias="0.051"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintWidth_percent="0.9" />
|
||||
|
||||
<!-- Leitfrage -->
|
||||
<TextView
|
||||
android:id="@+id/question"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHeight_percent="0.15"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="8dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/textView"
|
||||
app:layout_constraintVertical_bias="0.0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintWidth_percent="0.9" />
|
||||
|
||||
<!-- FIXE ICON-LEISTE (bleibt stehen) -->
|
||||
<LinearLayout
|
||||
android:id="@+id/glass_header"
|
||||
android:layout_width="0dp"
|
||||
@ -48,7 +51,6 @@
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintWidth_percent="1" />
|
||||
|
||||
<!-- Scrollbarer Bereich NUR für Symptome + Kreise -->
|
||||
<ScrollView
|
||||
android:id="@+id/glassScroll"
|
||||
android:layout_width="0dp"
|
||||
@ -73,15 +75,12 @@
|
||||
android:textStyle="bold" />
|
||||
</ScrollView>
|
||||
|
||||
<!-- Buttons unten -->
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/Qprev"
|
||||
android:layout_width="@dimen/nav_btn_size"
|
||||
android:layout_height="@dimen/nav_btn_size"
|
||||
android:layout_marginStart="20dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:text=""
|
||||
android:textAllCaps="false"
|
||||
app:icon="@drawable/ic_chevron_left"
|
||||
app:iconTint="@color/btn_nav_left_icon_tint"
|
||||
app:iconSize="@dimen/nav_icon_size"
|
||||
@ -92,15 +91,12 @@
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
<!-- Weiter -->
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/Qnext"
|
||||
android:layout_width="@dimen/nav_btn_size"
|
||||
android:layout_height="@dimen/nav_btn_size"
|
||||
android:layout_marginEnd="20dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:text=""
|
||||
android:textAllCaps="false"
|
||||
app:icon="@drawable/ic_chevron_right"
|
||||
app:iconTint="@color/btn_nav_right_icon_tint"
|
||||
app:iconSize="@dimen/nav_icon_size"
|
||||
|
||||
@ -5,9 +5,14 @@
|
||||
android:id="@+id/main"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".MainActivity"
|
||||
tools:layout_editor_absoluteX="11dp"
|
||||
tools:layout_editor_absoluteY="107dp">
|
||||
tools:context=".MainActivity">
|
||||
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
android:id="@+id/gTop"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
app:layout_constraintGuide_begin="32dp" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/Qprev"
|
||||
@ -27,7 +32,6 @@
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
<!-- Weiter -->
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/Qnext"
|
||||
android:layout_width="@dimen/nav_btn_size"
|
||||
@ -46,7 +50,34 @@
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
|
||||
<!-- ScrollView füllt den Raum zwischen Frage und Buttons -->
|
||||
<TextView
|
||||
android:id="@+id/textView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:textStyle="bold"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/gTop"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintWidth_percent="0.9" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/question"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:textStyle="bold"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="8dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/textView"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintWidth_percent="0.9" />
|
||||
|
||||
<ScrollView
|
||||
android:id="@+id/scrollView"
|
||||
android:layout_width="0dp"
|
||||
@ -56,8 +87,8 @@
|
||||
android:layout_marginBottom="8dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/question"
|
||||
app:layout_constraintBottom_toTopOf="@+id/Qnext"
|
||||
app:layout_constraintTop_toBottomOf="@id/question"
|
||||
app:layout_constraintBottom_toTopOf="@id/Qnext"
|
||||
app:layout_constraintWidth_percent="0.9">
|
||||
|
||||
<LinearLayout
|
||||
@ -68,32 +99,4 @@
|
||||
android:padding="16dp" />
|
||||
</ScrollView>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:gravity="center"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHeight_percent="0.15"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_bias="0.051"
|
||||
app:layout_constraintWidth_percent="0.9" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/question"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:gravity="center"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHeight_percent="0.15"
|
||||
app:layout_constraintHorizontal_bias="0.512"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/textView"
|
||||
app:layout_constraintVertical_bias="0.0"
|
||||
app:layout_constraintWidth_percent="0.9" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
@ -5,11 +5,15 @@
|
||||
android:id="@+id/main"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".MainActivity"
|
||||
tools:layout_editor_absoluteX="11dp"
|
||||
tools:layout_editor_absoluteY="107dp">
|
||||
tools:context=".MainActivity">
|
||||
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
android:id="@+id/gTop"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
app:layout_constraintGuide_begin="32dp" />
|
||||
|
||||
<!-- Zurück -->
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/Qprev"
|
||||
android:layout_width="@dimen/nav_btn_size"
|
||||
@ -28,7 +32,6 @@
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
<!-- Weiter -->
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/Qnext"
|
||||
android:layout_width="@dimen/nav_btn_size"
|
||||
@ -47,10 +50,34 @@
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:textStyle="bold"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/gTop"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintWidth_percent="0.9" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/question"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:textStyle="bold"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="8dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/textView"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintWidth_percent="0.9" />
|
||||
|
||||
<!-- SCROLLBEREICH für die Radio-Optionen:
|
||||
füllt den Platz zwischen Frage und Buttons, scrollt bei Bedarf -->
|
||||
<ScrollView
|
||||
android:id="@+id/radioScroll"
|
||||
android:layout_width="0dp"
|
||||
@ -65,7 +92,6 @@
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintWidth_percent="0.80">
|
||||
|
||||
<!-- Die RadioGroup bleibt gleich, ist jetzt aber scrollfähig -->
|
||||
<RadioGroup
|
||||
android:id="@+id/RadioGroup"
|
||||
android:layout_width="match_parent"
|
||||
@ -74,33 +100,4 @@
|
||||
android:padding="8dp" />
|
||||
</ScrollView>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:gravity="center"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHeight_percent="0.15"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_bias="0.051"
|
||||
app:layout_constraintWidth_percent="0.9" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/question"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:gravity="center"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHeight_percent="0.15"
|
||||
app:layout_constraintHorizontal_bias="0.512"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/textView"
|
||||
app:layout_constraintVertical_bias="0.0"
|
||||
app:layout_constraintWidth_percent="0.9" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
@ -5,6 +5,13 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
android:id="@+id/gTop"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
app:layout_constraintGuide_begin="32dp" />
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/string_spinner"
|
||||
android:layout_width="0dp"
|
||||
@ -33,7 +40,6 @@
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
<!-- Weiter -->
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/Qnext"
|
||||
android:layout_width="@dimen/nav_btn_size"
|
||||
@ -55,30 +61,29 @@
|
||||
<TextView
|
||||
android:id="@+id/textView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHeight_percent="0.15"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/gTop"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_bias="0.051"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintWidth_percent="0.9" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/question"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHeight_percent="0.15"
|
||||
app:layout_constraintHorizontal_bias="0.512"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="8dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/textView"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/textView"
|
||||
app:layout_constraintVertical_bias="0.0"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintWidth_percent="0.9" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
@ -5,6 +5,13 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
android:id="@+id/gTop"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
app:layout_constraintGuide_begin="32dp" />
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/value_spinner"
|
||||
android:layout_width="0dp"
|
||||
@ -13,7 +20,7 @@
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.495"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/question"
|
||||
app:layout_constraintTop_toBottomOf="@id/question"
|
||||
app:layout_constraintVertical_bias="0.027"
|
||||
app:layout_constraintWidth_percent="0.70" />
|
||||
|
||||
@ -35,7 +42,6 @@
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
<!-- Weiter -->
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/Qnext"
|
||||
android:layout_width="@dimen/nav_btn_size"
|
||||
@ -57,31 +63,29 @@
|
||||
<TextView
|
||||
android:id="@+id/textView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHeight_percent="0.15"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/gTop"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_bias="0.051"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintWidth_percent="0.9" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/question"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHeight_percent="0.15"
|
||||
app:layout_constraintHorizontal_bias="0.512"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="8dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/textView"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/textView"
|
||||
app:layout_constraintVertical_bias="0.0"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintWidth_percent="0.9" />
|
||||
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
Reference in New Issue
Block a user