Files
Questionnaire-App/app/src/main/java/com/dano/test1/HandlerOpeningScreen.kt
2025-09-29 11:59:51 +02:00

569 lines
24 KiB
Kotlin

package com.dano.test1
import android.content.res.ColorStateList
import android.graphics.Color
import android.os.Handler
import android.os.Looper
import android.util.TypedValue
import android.view.Gravity
import android.view.View
import android.widget.*
import com.google.android.material.button.MaterialButton
import org.json.JSONArray
import org.json.JSONObject
import java.io.File
import java.util.concurrent.TimeUnit
var RHS_POINTS: Int? = null
class HandlerOpeningScreen(private val activity: MainActivity) {
private var languageID: String = "GERMAN"
private lateinit var editText: EditText
private lateinit var coachEditText: EditText
private lateinit var spinner: Spinner
private lateinit var textView: TextView
private lateinit var buttonContainer: LinearLayout
private lateinit var buttonLoad: Button
private lateinit var saveButton: Button
private lateinit var editButton: Button
private lateinit var uploadButton: Button
private lateinit var downloadButton: Button
private lateinit var databaseButton: Button
private lateinit var statusSession: TextView
private lateinit var statusOnline: TextView
private val dynamicButtons = mutableListOf<Button>()
private val questionnaireFiles = mutableMapOf<Button, String>()
private data class CardParts(
val title: TextView,
val subtitle: TextView,
val chip: TextView
)
private val cardParts = mutableMapOf<Button, CardParts>()
private val buttonPoints: MutableMap<String, Int> = mutableMapOf()
private var questionnaireEntries: List<QuestionItem.QuestionnaireEntry> = emptyList()
private var uiFreeze: Boolean = false
private val uiHandler = Handler(Looper.getMainLooper())
private val statusTicker = object : Runnable {
override fun run() {
updateStatusStrip()
uiHandler.postDelayed(this, 60_000)
}
}
private val STROKE_ENABLED = Color.parseColor("#8C79F2")
private val STROKE_DISABLED = Color.parseColor("#D8D3F5")
private fun t(id: String) = LanguageManager.getText(languageID, id)
fun init() {
activity.setContentView(R.layout.opening_screen)
bindViews()
loadQuestionnaireOrder()
createQuestionnaireButtons()
restorePreviousClientCode()
lockCoachCodeField()
setupLanguageSpinner()
setupLoadButton()
setupSaveButton()
setupEditButtonHandler()
setupUploadButton()
setupDownloadButton()
setupDatabaseButtonHandler()
uiHandler.removeCallbacks(statusTicker)
updateStatusStrip()
uiHandler.post(statusTicker)
val pathExists = File("/data/data/com.dano.test1/databases/questionnaire_database").exists()
updateMainButtonsState(pathExists)
if (pathExists && !editText.text.isNullOrBlank()) buttonLoad.performClick()
}
private fun bindViews() {
editText = activity.findViewById(R.id.editText)
coachEditText = activity.findViewById(R.id.coachEditText)
spinner = activity.findViewById(R.id.string_spinner1)
textView = activity.findViewById(R.id.textView)
buttonContainer = activity.findViewById(R.id.buttonContainer)
buttonLoad = activity.findViewById(R.id.loadButton)
saveButton = activity.findViewById(R.id.saveButton)
editButton = activity.findViewById(R.id.editButton)
uploadButton = activity.findViewById(R.id.uploadButton)
downloadButton = activity.findViewById(R.id.downloadButton)
databaseButton = activity.findViewById(R.id.databaseButton)
statusSession = activity.findViewById(R.id.statusSession)
statusOnline = activity.findViewById(R.id.statusOnline)
val tag = editText.tag as? String ?: ""
editText.hint = t(tag)
val coachTag = coachEditText.tag as? String ?: ""
coachEditText.hint = t(coachTag)
textView.text = t("example_text")
}
private fun loadQuestionnaireOrder() {
try {
val inputStream = activity.assets.open("questionnaire_order.json")
val json = inputStream.bufferedReader().use { it.readText() }
val jsonArray = JSONArray(json)
questionnaireEntries = (0 until jsonArray.length()).map { i ->
val obj = jsonArray.getJSONObject(i)
val file = obj.getString("file")
val conditionObj = obj.optJSONObject("condition")
val condition = parseCondition(conditionObj)
val showPoints = obj.optBoolean("showPoints", false)
QuestionItem.QuestionnaireEntry(file, condition, showPoints)
}
} catch (_: Exception) {
questionnaireEntries = emptyList()
}
}
private fun parseCondition(conditionObj: JSONObject?): QuestionItem.Condition? {
if (conditionObj == null) return null
if (conditionObj.has("anyOf")) {
val arr = conditionObj.optJSONArray("anyOf") ?: JSONArray()
val conditions = mutableListOf<QuestionItem.Condition>()
for (i in 0 until arr.length()) {
val sub = arr.optJSONObject(i)
parseCondition(sub)?.let { conditions.add(it) }
}
return QuestionItem.Condition.AnyOf(conditions)
}
if (conditionObj.has("alwaysAvailable") && conditionObj.optBoolean("alwaysAvailable", false)) {
return QuestionItem.Condition.AlwaysAvailable
}
val requiresList = mutableListOf<String>()
if (conditionObj.has("requiresCompleted")) {
val reqArr = conditionObj.optJSONArray("requiresCompleted")
if (reqArr != null) {
for (i in 0 until reqArr.length()) requiresList.add(reqArr.optString(i))
} else {
conditionObj.optString("requiresCompleted")?.let { if (it.isNotBlank()) requiresList.add(it) }
}
}
val questionnaire = conditionObj.optString("questionnaire", null)
val questionId = conditionObj.optString("questionId", null)
val operator = conditionObj.optString("operator", null)
val value = conditionObj.optString("value", null)
val hasQuestionCheck =
!questionnaire.isNullOrBlank() && !questionId.isNullOrBlank() && !operator.isNullOrBlank() && value != null
return when {
requiresList.isNotEmpty() && hasQuestionCheck ->
QuestionItem.Condition.Combined(requiresList, QuestionItem.Condition.QuestionCondition(questionnaire!!, questionId!!, operator!!, value!!))
hasQuestionCheck ->
QuestionItem.Condition.QuestionCondition(questionnaire!!, questionId!!, operator!!, value!!)
requiresList.isNotEmpty() ->
QuestionItem.Condition.RequiresCompleted(requiresList)
else -> null
}
}
private fun createQuestionnaireButtons() {
buttonContainer.removeAllViews()
dynamicButtons.clear()
questionnaireFiles.clear()
cardParts.clear()
val vMargin = dp(8)
val startEnabled = mutableListOf<Button>()
questionnaireEntries.forEachIndexed { index, entry ->
val row = FrameLayout(activity).apply {
layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
).also { it.setMargins(0, vMargin, 0, vMargin) }
}
val btn = MaterialButton(activity).apply {
layoutParams = FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
dp(84)
)
text = ""
isAllCaps = false
setMinimumWidth(0)
setMinimumHeight(0)
cornerRadius = dp(22)
strokeWidth = dp(1)
strokeColor = ColorStateList.valueOf(STROKE_DISABLED)
backgroundTintList = ColorStateList.valueOf(Color.WHITE)
rippleColor = ColorStateList.valueOf(Color.parseColor("#22000000"))
gravity = Gravity.START or Gravity.CENTER_VERTICAL
setPadding(dp(20), 0, dp(20), 0)
id = View.generateViewId()
setOnClickListener {
GlobalValues.LAST_CLIENT_CODE = GlobalValues.LOADED_CLIENT_CODE
val fileName = questionnaireFiles[this] ?: return@setOnClickListener
val questionnaire = QuestionnaireGeneric(fileName)
startQuestionnaire(questionnaire)
applySetButtonsEnabled(dynamicButtons.filter { it == this }, allowCompleted = false, force = false)
}
}
val textColumn = LinearLayout(activity).apply {
orientation = LinearLayout.VERTICAL
layoutParams = FrameLayout.LayoutParams(
FrameLayout.LayoutParams.WRAP_CONTENT,
FrameLayout.LayoutParams.WRAP_CONTENT,
Gravity.START or Gravity.CENTER_VERTICAL
).also { it.marginStart = dp(24) }
}
val tvTitle = TextView(activity).apply {
setTextSize(TypedValue.COMPLEX_UNIT_SP, 18f)
setTextColor(Color.parseColor("#2F2A49"))
setPadding(0, dp(6), 0, dp(2))
}
val tvSubtitle = TextView(activity).apply {
setTextSize(TypedValue.COMPLEX_UNIT_SP, 13f)
setTextColor(Color.parseColor("#7B7794"))
visibility = View.GONE
}
textColumn.addView(tvTitle)
textColumn.addView(tvSubtitle)
val chip = TextView(activity).apply {
setPadding(dp(14), dp(8), dp(14), dp(8))
setTextColor(Color.WHITE)
text = t("start")
setBackgroundResource(R.drawable.bg_chip_amber)
setTextSize(TypedValue.COMPLEX_UNIT_SP, 14f)
layoutParams = FrameLayout.LayoutParams(
FrameLayout.LayoutParams.WRAP_CONTENT,
FrameLayout.LayoutParams.WRAP_CONTENT,
Gravity.END or Gravity.CENTER_VERTICAL
).also { it.marginEnd = dp(16) }
}
row.addView(btn)
row.addView(textColumn)
row.addView(chip)
buttonContainer.addView(row)
dynamicButtons.add(btn)
questionnaireFiles[btn] = entry.file
cardParts[btn] = CardParts(tvTitle, tvSubtitle, chip)
tvTitle.text = "Questionnaire ${index + 1}"
if (entry.condition is QuestionItem.Condition.AlwaysAvailable) startEnabled.add(btn)
}
applyUpdateButtonTexts(force = false)
applySetButtonsEnabled(startEnabled, allowCompleted = false, force = false)
}
private fun restorePreviousClientCode() {
val username = TokenStore.getUsername(activity)
if (!username.isNullOrBlank()) {
coachEditText.setText(username)
lockCoachCodeField()
return
}
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 {
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
}
spinner.adapter = adapter
spinner.setSelection(languages.indexOf(languageID))
spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>, view: View?, position: Int, id: Long) {
languageID = languages[position]
applyUpdateButtonTexts(force = false)
val hintTag = editText.tag as? String ?: ""
editText.hint = t(hintTag)
val coachTag = coachEditText.tag as? String ?: ""
coachEditText.hint = t(coachTag)
}
override fun onNothingSelected(parent: AdapterView<*>) {}
}
}
private fun setupLoadButton() {
LoadButtonHandler(
activity = activity,
loadButton = buttonLoad,
editText = editText,
languageIDProvider = { languageID },
questionnaireEntriesProvider = { questionnaireEntries },
dynamicButtonsProvider = { dynamicButtons },
buttonPoints = buttonPoints,
updateButtonTexts = { applyUpdateButtonTexts(force = false) },
setButtonsEnabled = { list -> applySetButtonsEnabled(list, allowCompleted = false, force = false) },
updateMainButtonsState = { updateMainButtonsState(it) },
).setup()
}
private fun applyUpdateButtonTexts(force: Boolean) {
if (uiFreeze && !force) return
updateStatusStrip()
questionnaireFiles.forEach { (button, fileName) ->
val entry = questionnaireEntries.firstOrNull { it.file == fileName }
val key = fileName.substringAfter("questionnaire_").substringAfter("_").removeSuffix(".json")
val titleText = t(key)
val parts = cardParts[button] ?: return@forEach
parts.title.text = titleText
val points = buttonPoints.entries.firstOrNull { fileName.contains(it.key, ignoreCase = true) }?.value
val completed = isCompleted(button)
val enabled = button.isEnabled
val locked = !enabled && !completed
setClickableStroke(button, enabled)
if (locked) {
setLockedAppearance(button, true)
parts.title.setTextColor(Color.WHITE)
parts.subtitle.setTextColor(Color.parseColor("#E0E0E0"))
} else {
setLockedAppearance(button, false)
parts.title.setTextColor(Color.parseColor("#2F2A49"))
parts.subtitle.setTextColor(Color.parseColor("#7B7794"))
applyTintForButton(button, points, emphasize = enabled)
}
if (entry?.showPoints == true && points != null) {
parts.subtitle.visibility = View.VISIBLE
parts.subtitle.text = "${t("points")}: $points"
} else {
parts.subtitle.visibility = View.GONE
parts.subtitle.text = ""
}
when {
completed -> {
parts.chip.text = t("done")
parts.chip.setBackgroundResource(R.drawable.bg_chip_green)
parts.chip.setTextColor(Color.WHITE)
}
enabled -> {
parts.chip.text = t("start")
parts.chip.setBackgroundResource(R.drawable.bg_chip_amber)
parts.chip.setTextColor(Color.WHITE)
}
else -> {
parts.chip.text = t("locked")
parts.chip.setBackgroundResource(R.drawable.bg_chip_grey)
parts.chip.setTextColor(Color.parseColor("#2F2A49"))
}
}
}
buttonLoad.text = t("load")
saveButton.text = t("save")
editButton.text = t("edit")
uploadButton.text = t("upload")
downloadButton.text = t("download")
databaseButton.text = t("database")
val hintTag = editText.tag as? String ?: ""
editText.hint = t(hintTag)
val coachTag = coachEditText.tag as? String ?: ""
coachEditText.hint = t(coachTag)
textView.text = t("example_text")
}
private fun applySetButtonsEnabled(enabledButtons: List<Button>, allowCompleted: Boolean, force: Boolean) {
if (uiFreeze && !force) return
questionnaireFiles.keys.forEach { button ->
val completed = isCompleted(button)
val isAllowed = enabledButtons.contains(button)
val shouldEnable = if (allowCompleted) isAllowed else isAllowed && !completed
val locked = !shouldEnable && !completed
button.isEnabled = shouldEnable
button.alpha = if (completed || shouldEnable) 1.0f else 0.6f
setClickableStroke(button, shouldEnable)
cardParts[button]?.let { parts ->
if (locked) {
setLockedAppearance(button, true)
parts.title.setTextColor(Color.WHITE)
parts.subtitle.setTextColor(Color.parseColor("#E0E0E0"))
} else {
setLockedAppearance(button, false)
parts.title.setTextColor(Color.parseColor("#2F2A49"))
parts.subtitle.setTextColor(Color.parseColor("#7B7794"))
applyTintForButton(button, getPointsForButton(button), emphasize = shouldEnable)
}
when {
completed -> {
parts.chip.text = t("done")
parts.chip.setBackgroundResource(R.drawable.bg_chip_green)
parts.chip.setTextColor(Color.WHITE)
}
shouldEnable -> {
parts.chip.text = t("start")
parts.chip.setBackgroundResource(R.drawable.bg_chip_amber)
parts.chip.setTextColor(Color.WHITE)
}
else -> {
parts.chip.text = t("locked")
parts.chip.setBackgroundResource(R.drawable.bg_chip_grey)
parts.chip.setTextColor(Color.parseColor("#2F2A49"))
}
}
}
}
}
private fun startQuestionnaire(questionnaire: QuestionnaireBase<*>) {
activity.startQuestionnaire(questionnaire, languageID)
}
fun onBackPressed(): Boolean = false
private fun setupSaveButton() {
SaveButtonHandler(
activity = activity,
saveButton = saveButton,
editText = editText,
languageIDProvider = { languageID }
).setup()
}
private fun setupEditButtonHandler() {
EditButtonHandler(
activity = activity,
editButton = editButton,
editText = editText,
languageIDProvider = { languageID },
questionnaireFiles = questionnaireFiles,
buttonPoints = buttonPoints,
updateButtonTexts = { applyUpdateButtonTexts(force = true) },
setButtonsEnabled = { list, allowCompleted ->
applySetButtonsEnabled(list, allowCompleted, force = true)
},
setUiFreeze = { freeze -> uiFreeze = freeze },
triggerLoad = { buttonLoad.performClick() }
).setup()
}
private fun setupUploadButton() {
uploadButton.text = t("upload")
uploadButton.setOnClickListener {
val token = TokenStore.getToken(activity)
if (token.isNullOrBlank()) {
Toast.makeText(activity, t("login_required") ?: "Bitte zuerst einloggen", Toast.LENGTH_LONG).show()
return@setOnClickListener
}
GlobalValues.LAST_CLIENT_CODE = editText.text.toString().trim()
DatabaseUploader.uploadDatabaseWithToken(activity, token)
}
}
private fun setupDownloadButton() {
downloadButton.text = t("download")
downloadButton.setOnClickListener {
Toast.makeText(activity, t("login_required") ?: "Bitte zuerst einloggen", Toast.LENGTH_SHORT).show()
}
}
private fun setupDatabaseButtonHandler() {
DatabaseButtonHandler(
activity = activity,
databaseButton = databaseButton,
onClose = { init() },
languageIDProvider = { languageID }
).setup()
}
private fun updateMainButtonsState(isDatabaseAvailable: Boolean) {
listOf(buttonLoad, saveButton, editButton, databaseButton).forEach { b ->
b.isEnabled = isDatabaseAvailable
b.alpha = if (isDatabaseAvailable) 1.0f else 0.5f
}
}
private fun dp(v: Int): Int = (v * activity.resources.displayMetrics.density).toInt()
private fun isCompleted(button: Button): Boolean {
val fileName = questionnaireFiles[button] ?: return false
return buttonPoints.keys.any { k ->
fileName.contains(k, ignoreCase = true) || k.contains(fileName, ignoreCase = true)
}
}
private fun getPointsForButton(button: Button): Int? {
val fileName = questionnaireFiles[button] ?: return null
return buttonPoints.entries.firstOrNull { (k, _) ->
fileName.contains(k, ignoreCase = true) || k.contains(fileName, ignoreCase = true)
}?.value
}
private fun setLockedAppearance(button: Button, locked: Boolean) {
val mb = button as? MaterialButton ?: return
if (locked) {
mb.backgroundTintList = ColorStateList.valueOf(Color.BLACK)
mb.strokeColor = ColorStateList.valueOf(STROKE_DISABLED)
} else {
mb.backgroundTintList = ColorStateList.valueOf(Color.WHITE)
mb.strokeColor = ColorStateList.valueOf(if (button.isEnabled) STROKE_ENABLED else STROKE_DISABLED)
}
}
private fun resetTint(button: Button) {
val mb = button as? MaterialButton ?: return
mb.backgroundTintList = ColorStateList.valueOf(Color.WHITE)
mb.strokeColor = ColorStateList.valueOf(STROKE_DISABLED)
}
private fun applyTintForButton(button: Button, points: Int?, emphasize: Boolean) {
val file = questionnaireFiles[button] ?: return
val entry = questionnaireEntries.firstOrNull { it.file == file }
if (entry?.showPoints != true) {
val mb = button as? MaterialButton ?: return
mb.backgroundTintList = ColorStateList.valueOf(if (emphasize) Color.parseColor("#F1EEFF") else Color.WHITE)
mb.strokeColor = ColorStateList.valueOf(if (emphasize) STROKE_ENABLED else STROKE_DISABLED)
return
}
setScoreTint(button, points, emphasize)
}
private fun setClickableStroke(button: Button, enabled: Boolean) {
val mb = button as? MaterialButton ?: return
mb.strokeWidth = if (enabled) dp(2) else dp(1)
mb.strokeColor = ColorStateList.valueOf(if (enabled) STROKE_ENABLED else STROKE_DISABLED)
}
private fun setScoreTint(button: Button, points: Int?, emphasize: Boolean) {
val mb = button as? MaterialButton ?: return
val bg = when {
points == null && emphasize -> Color.parseColor("#F1EEFF")
points == null -> Color.parseColor("#FFFFFF")
points in 0..12 && emphasize -> Color.parseColor("#C8E6C9")
points in 0..12 -> Color.parseColor("#E8F5E9")
points in 13..36 && emphasize -> Color.parseColor("#FFE0B2")
points in 13..36 -> Color.parseColor("#FFF8E1")
points >= 37 && emphasize -> Color.parseColor("#FFCDD2")
else -> Color.parseColor("#FFEBEE")
}
mb.backgroundTintList = ColorStateList.valueOf(bg)
mb.strokeColor = ColorStateList.valueOf(if (emphasize) STROKE_ENABLED else STROKE_DISABLED)
}
private fun updateStatusStrip() {
val ts = TokenStore.getLoginTimestamp(activity)
val ageMs = if (ts > 0L) (System.currentTimeMillis() - ts) else 0L
val h = TimeUnit.MILLISECONDS.toHours(ageMs)
val m = TimeUnit.MILLISECONDS.toMinutes(ageMs) - h * 60
statusSession.text = if (ts > 0L) "Session: ${h}h ${m}m" else "Session: —"
val online = NetworkUtils.isOnline(activity)
statusOnline.text = if (online) "Online" else "Offline"
statusOnline.setTextColor(if (online) Color.parseColor("#2E7D32") else Color.parseColor("#C62828"))
}
fun refreshHeaderStatusLive() {
applyUpdateButtonTexts(force = true)
}
private fun lockCoachCodeField() {
coachEditText.isFocusable = false
coachEditText.isFocusableInTouchMode = false
coachEditText.isCursorVisible = false
coachEditText.keyListener = null
coachEditText.isLongClickable = false
coachEditText.isClickable = false
coachEditText.setBackgroundResource(R.drawable.bg_field_locked)
coachEditText.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.ic_lock_24, 0)
coachEditText.compoundDrawablePadding = dp(8)
coachEditText.alpha = 0.95f
}
}