Compare commits

..

4 Commits

12 changed files with 852 additions and 166 deletions

View File

@ -41,6 +41,11 @@
</intent-filter>
</activity>
<activity
android:name=".ui.DevSettingsActivity"
android:exported="false"
android:parentActivityName=".MainActivity" />
</application>
</manifest>

View File

@ -0,0 +1,522 @@
package com.dano.test1.questionnaire
import android.graphics.Color
import android.text.Editable
import android.text.TextWatcher
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.*
import androidx.constraintlayout.widget.ConstraintLayout
import com.dano.test1.LanguageManager
import com.dano.test1.LocalizationHelper
import com.dano.test1.MainActivity
import com.dano.test1.R
import com.dano.test1.network.TokenStore
import com.dano.test1.utils.ViewUtils
import com.google.android.material.button.MaterialButton
import com.google.android.material.card.MaterialCardView
import kotlinx.coroutines.*
class QuestionnaireAllInOne(private val questionnaireFileName: String) : QuestionnaireBase<Unit>() {
private data class Section(
val index: Int,
val question: QuestionItem,
val card: MaterialCardView,
val sectionView: View,
val handler: QuestionHandler?
)
private val sections = mutableListOf<Section>()
private lateinit var container: LinearLayout
private lateinit var btnSave: MaterialButton
private var setupComplete = false
override fun startQuestionnaire() {
val (meta, questionsList) = loadQuestionnaireFromJson(questionnaireFileName)
questionnaireMeta = meta
questions = questionsList
currentIndex = 0
buildAllInOneUi()
}
override fun showCurrentQuestion() {
// Not used in all-in-one mode
}
private fun buildAllInOneUi() {
context.setContentView(R.layout.questionnaire_all_in_one)
container = context.findViewById(R.id.questionContainer)
btnSave = context.findViewById(R.id.btnSave)
val btnBack = context.findViewById<MaterialButton>(R.id.btnBack)
val progressBar = context.findViewById<ProgressBar>(R.id.progressBar)
btnSave.text = LanguageManager.getText(languageID, "save")
ViewUtils.setTextSizePercentOfScreenHeight(btnSave, 0.025f)
btnSave.isEnabled = false
btnSave.alpha = 0.5f
btnBack.setOnClickListener {
(context as? MainActivity)?.finishQuestionnaire()
}
val inflater = LayoutInflater.from(context)
for ((idx, question) in questions.withIndex()) {
if (question is QuestionItem.LastPage) continue
val layoutResId = getLayoutResId(question.layout ?: "default_layout")
if (layoutResId == 0) continue
val sectionView = inflater.inflate(layoutResId, container, false)
adaptLayoutForEmbedding(sectionView)
LocalizationHelper.localizeViewTree(sectionView, languageID)
val card = wrapInCard(sectionView)
container.addView(card)
val handler = createEmbeddedHandler(question)
handler?.bind(sectionView, question)
reduceTextSizes(sectionView)
sections.add(Section(idx, question, card, sectionView, handler))
}
installChangeListeners()
recalculateVisibility()
// Allow DB restores to finish before enabling interaction tracking
container.postDelayed({
setupComplete = true
recalculateVisibility()
updateSaveButtonState()
}, 600)
btnSave.setOnClickListener {
handleSave(progressBar)
}
}
private fun createEmbeddedHandler(question: QuestionItem): QuestionHandler? {
val noop = {}
val noopId: (String) -> Unit = {}
val toast: (String) -> Unit = { showToast(it) }
val metaId = questionnaireMeta.id
return when (question) {
is QuestionItem.RadioQuestion ->
com.dano.test1.questionnaire.handlers.HandlerRadioQuestion(
context, answers, points, languageID, noop, noop, noopId, toast, metaId
)
is QuestionItem.ClientCoachCodeQuestion ->
com.dano.test1.questionnaire.handlers.HandlerClientCoachCode(
answers, languageID, noop, noop, toast
)
is QuestionItem.DateSpinnerQuestion ->
com.dano.test1.questionnaire.handlers.HandlerDateSpinner(
context, answers, languageID, noop, noop, toast, metaId
)
is QuestionItem.ValueSpinnerQuestion ->
com.dano.test1.questionnaire.handlers.HandlerValueSpinner(
context, answers, languageID, noop, noop, noopId, toast, metaId
)
is QuestionItem.GlassScaleQuestion ->
com.dano.test1.questionnaire.handlers.HandlerGlassScaleQuestion(
context, answers, points, languageID, noop, noop, toast, metaId
)
is QuestionItem.ClientNotSigned ->
com.dano.test1.questionnaire.handlers.HandlerClientNotSigned(
answers, languageID, noop, noop, toast
)
is QuestionItem.StringSpinnerQuestion ->
com.dano.test1.questionnaire.handlers.HandlerStringSpinner(
context, answers, languageID, noop, noop, toast, metaId
)
is QuestionItem.MultiCheckboxQuestion ->
com.dano.test1.questionnaire.handlers.HandlerMultiCheckboxQuestion(
context, answers, points, languageID, noop, noop, toast, metaId
)
else -> null
}
}
private fun adaptLayoutForEmbedding(view: View) {
if (view is ConstraintLayout) {
view.layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
)
// Ensure the last field in the card is never clipped at the bottom
view.setPadding(
view.paddingLeft, view.paddingTop,
view.paddingRight, ViewUtils.dp(context, 12)
)
}
view.findViewById<View>(R.id.Qprev)?.visibility = View.GONE
view.findViewById<View>(R.id.Qnext)?.visibility = View.GONE
view.findViewById<View>(R.id.gTop)?.let { guideline ->
if (guideline is androidx.constraintlayout.widget.Guideline) {
val params = guideline.layoutParams as? ConstraintLayout.LayoutParams
params?.guideBegin = ViewUtils.dp(context, 8)
guideline.layoutParams = params
}
}
adaptInnerScrollView(view, R.id.radioScroll)
adaptInnerScrollView(view, R.id.scrollView)
adaptInnerScrollView(view, R.id.glassScroll)
view.findViewById<EditText>(R.id.client_code)?.let { et ->
val params = et.layoutParams as? ConstraintLayout.LayoutParams ?: return@let
if (params.matchConstraintPercentHeight > 0f) {
params.matchConstraintPercentHeight = 0f
params.height = ViewUtils.dp(context, 64)
et.layoutParams = params
}
}
view.findViewById<EditText>(R.id.coach_code)?.let { et ->
val params = et.layoutParams as? ConstraintLayout.LayoutParams ?: return@let
if (params.matchConstraintPercentHeight > 0f) {
params.matchConstraintPercentHeight = 0f
params.height = ViewUtils.dp(context, 64)
et.layoutParams = params
}
}
}
/**
* Scales down all TextView text sizes by [factor] after the handler has set them.
* Spinners manage their own adapter text sizes and are skipped.
*/
private fun reduceTextSizes(view: View, factor: Float = 0.82f) {
if (view is Spinner) return
if (view is TextView) {
val dm = view.context.resources.displayMetrics
val currentSp = view.textSize / dm.scaledDensity
view.setTextSize(TypedValue.COMPLEX_UNIT_SP, currentSp * factor)
}
if (view is ViewGroup) {
for (i in 0 until view.childCount) reduceTextSizes(view.getChildAt(i), factor)
}
}
private fun adaptInnerScrollView(root: View, scrollViewId: Int) {
val sv = root.findViewById<ScrollView>(scrollViewId) ?: return
val params = sv.layoutParams
if (params is ConstraintLayout.LayoutParams) {
params.height = ConstraintLayout.LayoutParams.WRAP_CONTENT
params.bottomToTop = ConstraintLayout.LayoutParams.UNSET
params.bottomToBottom = ConstraintLayout.LayoutParams.PARENT_ID
sv.layoutParams = params
}
sv.isNestedScrollingEnabled = false
}
private fun wrapInCard(sectionView: View): MaterialCardView {
val card = MaterialCardView(context).apply {
layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
).apply {
val margin = ViewUtils.dp(context, 6)
setMargins(0, margin, 0, margin)
}
radius = ViewUtils.dp(context, 16).toFloat()
cardElevation = ViewUtils.dp(context, 2).toFloat()
setCardBackgroundColor(Color.WHITE)
strokeColor = Color.parseColor("#D8D1F0")
strokeWidth = ViewUtils.dp(context, 1)
setContentPadding(
ViewUtils.dp(context, 4),
ViewUtils.dp(context, 8),
ViewUtils.dp(context, 4),
ViewUtils.dp(context, 12)
)
}
card.addView(sectionView)
return card
}
// ---- Visibility walk algorithm (reads from UI state, not answers map) ----
private fun recalculateVisibility() {
val visibleIndices = mutableSetOf<Int>()
var i = 0
while (i < questions.size) {
val q = questions[i]
if (q is QuestionItem.LastPage) { i++; continue }
visibleIndices.add(i)
val nextTarget = resolveNextFromUi(q)
when {
nextTarget != null -> {
if (questions.any { it is QuestionItem.LastPage && it.id == nextTarget }) {
break
}
val targetIdx = questions.indexOfFirst { it.id == nextTarget }
i = if (targetIdx != -1) targetIdx else i + 1
}
hasBranchingOptions(q) && !hasAnswerInUi(q) -> break
else -> i++
}
}
for (section in sections) {
val shouldShow = visibleIndices.contains(section.index)
section.card.visibility = if (shouldShow) View.VISIBLE else View.GONE
}
}
private fun resolveNextFromUi(q: QuestionItem): String? {
val section = sections.find { it.question === q } ?: return null
val view = section.sectionView
return when (q) {
is QuestionItem.RadioQuestion -> {
val radioGroup = view.findViewById<RadioGroup>(R.id.RadioGroup) ?: return null
val checkedId = radioGroup.checkedRadioButtonId
if (checkedId == -1) return null
val rb = radioGroup.findViewById<RadioButton>(checkedId) ?: return null
val key = rb.tag?.toString() ?: return null
q.options?.find { it.key == key }?.nextQuestionId
}
is QuestionItem.ValueSpinnerQuestion -> {
val spinner = view.findViewById<Spinner>(R.id.value_spinner) ?: return null
val selected = spinner.selectedItem?.toString() ?: return null
val prompt = LanguageManager.getText(languageID, "choose_answer")
if (selected == prompt) return null
val selectedVal = selected.toIntOrNull()
if (selectedVal != null) {
q.options?.find { it.value == selectedVal }?.nextQuestionId
} else null
}
else -> null
}
}
private fun hasBranchingOptions(q: QuestionItem): Boolean {
return when (q) {
is QuestionItem.RadioQuestion ->
q.options?.any { it.nextQuestionId != null } ?: false
is QuestionItem.ValueSpinnerQuestion ->
q.options?.any { it.nextQuestionId != null } ?: false
else -> false
}
}
private fun hasAnswerInUi(q: QuestionItem): Boolean {
val section = sections.find { it.question === q } ?: return false
val view = section.sectionView
return when (q) {
is QuestionItem.RadioQuestion -> {
val rg = view.findViewById<RadioGroup>(R.id.RadioGroup) ?: return false
rg.checkedRadioButtonId != -1
}
is QuestionItem.ValueSpinnerQuestion -> {
val spinner = view.findViewById<Spinner>(R.id.value_spinner) ?: return false
val selected = spinner.selectedItem?.toString()
val prompt = LanguageManager.getText(languageID, "choose_answer")
!selected.isNullOrEmpty() && selected != prompt
}
else -> true
}
}
// ---- Change listeners ----
private fun installChangeListeners() {
for (section in sections) {
val view = section.sectionView
when (section.question) {
is QuestionItem.RadioQuestion -> {
view.findViewById<RadioGroup>(R.id.RadioGroup)
?.setOnCheckedChangeListener { _, checkedId ->
if (!setupComplete || checkedId == -1) return@setOnCheckedChangeListener
recalculateVisibility()
updateSaveButtonState()
}
}
is QuestionItem.ValueSpinnerQuestion -> {
installSpinnerChangeListener(view, R.id.value_spinner)
}
is QuestionItem.StringSpinnerQuestion -> {
installSpinnerChangeListener(view, R.id.string_spinner)
}
is QuestionItem.DateSpinnerQuestion -> {
val listener = createSimpleSpinnerListener()
view.findViewById<Spinner>(R.id.spinner_value_day)?.onItemSelectedListener = listener
view.findViewById<Spinner>(R.id.spinner_value_month)?.onItemSelectedListener = listener
view.findViewById<Spinner>(R.id.spinner_value_year)?.onItemSelectedListener = listener
}
is QuestionItem.GlassScaleQuestion -> {
installGlassScaleClickListeners(view)
}
is QuestionItem.MultiCheckboxQuestion -> {
installCheckboxClickListeners(view)
}
is QuestionItem.ClientCoachCodeQuestion -> {
installEditTextWatcher(view, R.id.client_code)
installEditTextWatcher(view, R.id.coach_code)
}
is QuestionItem.ClientNotSigned -> {
installEditTextWatcher(view, R.id.coach_code)
}
else -> {}
}
}
}
private fun installSpinnerChangeListener(view: View, spinnerId: Int) {
view.findViewById<Spinner>(spinnerId)?.onItemSelectedListener =
object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(
parent: AdapterView<*>?, v: View?, pos: Int, id: Long
) {
if (!setupComplete) return
recalculateVisibility()
updateSaveButtonState()
}
override fun onNothingSelected(parent: AdapterView<*>?) {}
}
}
private fun createSimpleSpinnerListener(): AdapterView.OnItemSelectedListener {
return object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, v: View?, pos: Int, id: Long) {
if (!setupComplete) return
updateSaveButtonState()
}
override fun onNothingSelected(parent: AdapterView<*>?) {}
}
}
private fun installGlassScaleClickListeners(view: View) {
val table = view.findViewById<TableLayout>(R.id.glass_table) ?: return
for (r in 0 until table.childCount) {
val row = table.getChildAt(r) as? TableRow ?: continue
val radioGroup = row.getChildAt(1) as? RadioGroup ?: continue
for (c in 0 until radioGroup.childCount) {
val rb = getRadioFromChild(radioGroup.getChildAt(c)) ?: continue
rb.setOnClickListener {
if (setupComplete) updateSaveButtonState()
}
}
}
}
private fun getRadioFromChild(child: View): RadioButton? = when (child) {
is RadioButton -> child
is FrameLayout -> child.getChildAt(0) as? RadioButton
else -> null
}
private fun installCheckboxClickListeners(view: View) {
val cont = view.findViewById<LinearLayout>(R.id.CheckboxContainer) ?: return
for (i in 0 until cont.childCount) {
val cb = cont.getChildAt(i) as? CheckBox ?: continue
cb.setOnClickListener {
if (setupComplete) updateSaveButtonState()
}
}
}
private fun installEditTextWatcher(view: View, editTextId: Int) {
view.findViewById<EditText>(editTextId)?.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
override fun afterTextChanged(s: Editable?) {
if (!setupComplete) return
updateSaveButtonState()
}
})
}
// ---- Save button state ----
private fun updateSaveButtonState() {
val allValid = sections
.filter { it.card.visibility == View.VISIBLE }
.all { section ->
val handler = section.handler ?: return@all true
try { handler.validate() } catch (_: Exception) { false }
}
btnSave.isEnabled = allValid
btnSave.alpha = if (allValid) 1f else 0.5f
}
// ---- Save flow ----
private fun handleSave(progressBar: ProgressBar) {
val visibleSections = sections.filter { it.card.visibility == View.VISIBLE }
for (section in visibleSections) {
val handler = section.handler ?: continue
if (!handler.validate()) {
showToast(LanguageManager.getText(languageID, "fill_all_fields"))
scrollToSection(section)
return
}
}
for (section in visibleSections) {
if (section.question is QuestionItem.ClientCoachCodeQuestion) {
saveClientCoachCodeFromUi(section.sectionView)
}
section.handler?.saveAnswer()
}
showLoading(progressBar, true)
CoroutineScope(Dispatchers.IO).launch {
val startTime = System.currentTimeMillis()
saveAnswersToDatabase(answers, questionnaireMeta.id)
GlobalValues.INTEGRATION_INDEX = points.sum()
val clientCode = answers["client_code"] as? String
if (clientCode != null) {
GlobalValues.LAST_CLIENT_CODE = clientCode
GlobalValues.LOADED_CLIENT_CODE = clientCode
}
val elapsed = System.currentTimeMillis() - startTime
if (elapsed < 1500L) delay(1500L - elapsed)
withContext(Dispatchers.Main) {
showLoading(progressBar, false)
endQuestionnaire()
}
}
}
private fun saveClientCoachCodeFromUi(view: View) {
val clientCode = view.findViewById<EditText>(R.id.client_code)?.text?.toString() ?: ""
val coachCode = view.findViewById<EditText>(R.id.coach_code)?.text?.toString() ?: ""
GlobalValues.LAST_CLIENT_CODE = clientCode
answers["client_code"] = clientCode
answers["coach_code"] = TokenStore.getUsername(context) ?: coachCode
}
private fun showLoading(progressBar: ProgressBar, show: Boolean) {
progressBar.visibility = if (show) View.VISIBLE else View.GONE
btnSave.isEnabled = !show
btnSave.alpha = if (show) 0.5f else 1f
}
private fun scrollToSection(section: Section) {
val scrollView = context.findViewById<androidx.core.widget.NestedScrollView>(R.id.scrollContainer)
scrollView?.post {
scrollView.smoothScrollTo(0, section.card.top)
}
}
}

View File

@ -0,0 +1,9 @@
package com.dano.test1.questionnaire
object QuestionnaireProgressiveCallbacks {
@JvmStatic
fun maybeTrim(questionId: String?) {
// Intentional no-op: handlers call this during user interaction,
// but trimming is handled by each questionnaire variant itself.
}
}

View File

@ -0,0 +1,46 @@
package com.dano.test1.ui
import android.content.Context
/**
* Persists dev-only A/B overrides. Not a security boundary; values are for local QA.
* Use [effectiveVariant] when branching questionnaire or remote-config logic.
*/
object AbTestSettingsStore {
private const val PREF = "dev_ab_settings"
private const val KEY_OVERRIDE = "override_enabled"
private const val KEY_VARIANT = "variant"
const val VARIANT_NONE = "NONE"
const val VARIANT_A = "A"
const val VARIANT_B = "B"
fun isOverrideEnabled(context: Context): Boolean =
context.getSharedPreferences(PREF, Context.MODE_PRIVATE).getBoolean(KEY_OVERRIDE, false)
fun setOverrideEnabled(context: Context, enabled: Boolean) {
context.getSharedPreferences(PREF, Context.MODE_PRIVATE).edit()
.putBoolean(KEY_OVERRIDE, enabled)
.apply()
}
fun getVariant(context: Context): String =
context.getSharedPreferences(PREF, Context.MODE_PRIVATE).getString(KEY_VARIANT, VARIANT_NONE)
?: VARIANT_NONE
fun setVariant(context: Context, variant: String) {
context.getSharedPreferences(PREF, Context.MODE_PRIVATE).edit()
.putString(KEY_VARIANT, variant)
.apply()
}
/** Returns "A" or "B" when override is on and a branch is selected; otherwise null. */
fun effectiveVariant(context: Context): String? {
if (!isOverrideEnabled(context)) return null
return when (getVariant(context)) {
VARIANT_A -> "A"
VARIANT_B -> "B"
else -> null
}
}
}

View File

@ -7,8 +7,8 @@ import android.view.View
import android.widget.*
import com.dano.test1.data.ExcelExportService
import com.dano.test1.utils.ViewUtils
import androidx.appcompat.app.AppCompatActivity
import com.dano.test1.LanguageManager
import com.dano.test1.MainActivity
import com.dano.test1.MyApp
import com.dano.test1.R
import com.dano.test1.data.Client
@ -19,7 +19,7 @@ import kotlinx.coroutines.*
import org.json.JSONArray
class DatabaseButtonHandler(
private val activity: MainActivity,
private val activity: AppCompatActivity,
private val databaseButton: Button,
private val onClose: () -> Unit,
private val languageIDProvider: () -> String = { "GERMAN" }

View File

@ -0,0 +1,79 @@
package com.dano.test1.ui
import android.os.Bundle
import android.widget.Button
import android.widget.RadioButton
import android.widget.RadioGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.SwitchCompat
import androidx.appcompat.widget.Toolbar
import com.dano.test1.R
/**
* Dev-only A/B settings. Entry: client code [_dev_settings_] on the opening screen Load action.
* Not a security boundary (secret string is in the APK).
*/
class DevSettingsActivity : AppCompatActivity() {
private lateinit var switchOverride: SwitchCompat
private lateinit var radioGroup: RadioGroup
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_dev_settings)
val toolbar = findViewById<Toolbar>(R.id.devSettingsToolbar)
setSupportActionBar(toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
switchOverride = findViewById(R.id.switchAbOverride)
radioGroup = findViewById(R.id.radioGroupVariant)
applyPrefsToUi()
switchOverride.setOnCheckedChangeListener { _, isChecked ->
AbTestSettingsStore.setOverrideEnabled(this, isChecked)
updateRadiosEnabled(isChecked)
}
radioGroup.setOnCheckedChangeListener { _, checkedId ->
val variant = when (checkedId) {
R.id.radioVariantA -> AbTestSettingsStore.VARIANT_A
R.id.radioVariantB -> AbTestSettingsStore.VARIANT_B
else -> AbTestSettingsStore.VARIANT_NONE
}
AbTestSettingsStore.setVariant(this, variant)
}
val databaseButton = findViewById<Button>(R.id.databaseButton)
DatabaseButtonHandler(
activity = this,
databaseButton = databaseButton,
onClose = { recreate() }
).setup()
}
private fun applyPrefsToUi() {
val overrideOn = AbTestSettingsStore.isOverrideEnabled(this)
switchOverride.isChecked = overrideOn
updateRadiosEnabled(overrideOn)
when (AbTestSettingsStore.getVariant(this)) {
AbTestSettingsStore.VARIANT_A -> radioGroup.check(R.id.radioVariantA)
AbTestSettingsStore.VARIANT_B -> radioGroup.check(R.id.radioVariantB)
else -> radioGroup.check(R.id.radioVariantDefault)
}
}
private fun updateRadiosEnabled(enabled: Boolean) {
for (i in 0 until radioGroup.childCount) {
(radioGroup.getChildAt(i) as? RadioButton)?.isEnabled = enabled
}
radioGroup.alpha = if (enabled) 1f else 0.5f
}
override fun onSupportNavigateUp(): Boolean {
finish()
return true
}
}

View File

@ -1,97 +0,0 @@
package com.dano.test1.ui
import android.widget.Button
import android.widget.EditText
import android.widget.Toast
import com.dano.test1.LanguageManager
import com.dano.test1.MainActivity
import com.dano.test1.MyApp
import kotlinx.coroutines.*
import com.dano.test1.data.CompletedQuestionnaire
import com.dano.test1.questionnaire.GlobalValues
class EditButtonHandler(
private val activity: MainActivity,
private val editButton: Button,
private val editText: EditText,
private val languageIDProvider: () -> String,
private val questionnaireFiles: Map<Button, String>,
private val buttonPoints: MutableMap<String, Int>,
private val updateButtonTexts: () -> Unit,
private val setButtonsEnabled: (List<Button>, Boolean) -> Unit,
private val setUiFreeze: (Boolean) -> Unit,
private val triggerLoad: () -> Unit
) {
fun setup() {
editButton.text = LanguageManager.getText(languageIDProvider(), "edit")
editButton.setOnClickListener { handleEditButtonClick() }
}
private fun handleEditButtonClick() {
val typed = editText.text.toString().trim()
val desiredCode = when {
typed.isNotBlank() -> typed
!GlobalValues.LOADED_CLIENT_CODE.isNullOrBlank() -> GlobalValues.LOADED_CLIENT_CODE!!
else -> ""
}
if (desiredCode.isBlank()) {
val message = LanguageManager.getText(languageIDProvider(), "please_client_code")
Toast.makeText(activity, message, Toast.LENGTH_SHORT).show()
return
}
GlobalValues.LAST_CLIENT_CODE = desiredCode
val needLoad = GlobalValues.LOADED_CLIENT_CODE?.equals(desiredCode) != true
if (needLoad) {
setUiFreeze(true) // Zwischenzustände unterdrücken
triggerLoad()
}
CoroutineScope(Dispatchers.IO).launch {
val loadedOk = waitUntilClientLoaded(desiredCode, timeoutMs = 2500, stepMs = 50)
if (!loadedOk) {
withContext(Dispatchers.Main) {
val msg = LanguageManager.getText(languageIDProvider(), "open_client_via_load")
Toast.makeText(activity, msg, Toast.LENGTH_LONG).show()
setUiFreeze(false)
}
return@launch
}
val completedEntries: List<CompletedQuestionnaire> =
MyApp.database.completedQuestionnaireDao().getAllForClient(desiredCode)
val completedFiles = completedEntries.filter { it.isDone }.map { it.questionnaireId.lowercase() }
buttonPoints.clear()
for (entry in completedEntries) {
if (entry.isDone) {
buttonPoints[entry.questionnaireId] = entry.sumPoints ?: 0
}
}
withContext(Dispatchers.Main) {
updateButtonTexts()
val enabledButtons = questionnaireFiles.filter { (_, fileName) ->
completedFiles.any { completedId -> fileName.lowercase().contains(completedId) }
}.keys.toList()
setButtonsEnabled(enabledButtons, true)
setUiFreeze(false)
}
}
}
private suspend fun waitUntilClientLoaded(expectedCode: String, timeoutMs: Long, stepMs: Long): Boolean {
if (GlobalValues.LOADED_CLIENT_CODE?.equals(expectedCode) == true) return true
var waited = 0L
while (waited < timeoutMs) {
delay(stepMs)
waited += stepMs
if (GlobalValues.LOADED_CLIENT_CODE?.equals(expectedCode) == true) return true
}
return GlobalValues.LOADED_CLIENT_CODE?.equals(expectedCode) == true
}
}

View File

@ -20,6 +20,7 @@ import com.dano.test1.network.TokenStore
import com.dano.test1.questionnaire.GlobalValues
import com.dano.test1.questionnaire.QuestionItem
import com.dano.test1.questionnaire.QuestionnaireBase
import com.dano.test1.questionnaire.QuestionnaireAllInOne
import com.dano.test1.questionnaire.QuestionnaireGeneric
import com.google.android.material.button.MaterialButton
import org.json.JSONArray
@ -42,10 +43,8 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
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 SESSION_WARN_AFTER_MS = 12 * 60 * 60 * 1000L // 12h
@ -94,10 +93,8 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
setupLanguageSpinner()
setupLoadButton()
setupSaveButton()
setupEditButtonHandler()
setupUploadButton()
setupDownloadButton()
setupDatabaseButtonHandler()
uiHandler.removeCallbacks(statusTicker)
updateStatusStrip()
@ -123,13 +120,11 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
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)
downloadButton.visibility = View.GONE
databaseButton = activity.findViewById(R.id.databaseButton)
statusSession = activity.findViewById(R.id.statusSession)
statusOnline = activity.findViewById(R.id.statusOnline)
val tag = editText.tag as? String ?: ""
@ -231,7 +226,12 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
setOnClickListener {
GlobalValues.LAST_CLIENT_CODE = GlobalValues.LOADED_CLIENT_CODE
val fileName = questionnaireFiles[this] ?: return@setOnClickListener
val questionnaire = QuestionnaireGeneric(fileName)
val variant = AbTestSettingsStore.effectiveVariant(activity)
val questionnaire: QuestionnaireBase<*> = if (variant == "B") {
QuestionnaireAllInOne(fileName)
} else {
QuestionnaireGeneric(fileName)
}
startQuestionnaire(questionnaire)
applySetButtonsEnabled(dynamicButtons.filter { it == this }, allowCompleted = false, force = false)
}
@ -331,7 +331,7 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
dynamicButtonsProvider = { dynamicButtons },
buttonPoints = buttonPoints,
updateButtonTexts = { applyUpdateButtonTexts(force = false) },
setButtonsEnabled = { list -> applySetButtonsEnabled(list, allowCompleted = false, force = false) },
setButtonsEnabled = { list -> applySetButtonsEnabled(list, allowCompleted = true, force = false) },
updateMainButtonsState = { updateMainButtonsState(it) },
).setup()
}
@ -387,10 +387,8 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
}
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 ?: ""
@ -455,23 +453,6 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
).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 {
@ -533,17 +514,8 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
}
}
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 ->
listOf(buttonLoad, saveButton).forEach { b ->
b.isEnabled = isDatabaseAvailable
b.alpha = if (isDatabaseAvailable) 1.0f else 0.5f
}

View File

@ -1,5 +1,6 @@
package com.dano.test1.ui
import android.content.Intent
import android.widget.Button
import android.widget.EditText
import android.widget.Toast
@ -24,6 +25,11 @@ class LoadButtonHandler(
private val updateMainButtonsState: (Boolean) -> Unit,
) {
companion object {
/** Opening-screen client code that opens dev A/B settings (not a security boundary). */
private const val DEV_SETTINGS_SECRET = "_dev_settings_"
}
fun setup() {
loadButton.text = LanguageManager.getText(languageIDProvider(), "load")
loadButton.setOnClickListener { handleLoadButton() }
@ -37,6 +43,12 @@ class LoadButtonHandler(
return
}
if (inputText == DEV_SETTINGS_SECRET) {
editText.text.clear()
activity.startActivity(Intent(activity, DevSettingsActivity::class.java))
return
}
buttonPoints.clear()
setButtonsEnabled(emptyList()) // temporär sperren
updateButtonTexts() // Chips zeigen vorläufig „Gesperrt“
@ -148,10 +160,9 @@ class LoadButtonHandler(
(completedNorm.contains(targetNorm) || targetNorm.contains(completedNorm)) && completed.isDone
}
}
if (isCompleted) continue
val condMet = evaluateCondition(entry.condition, clientCode, completedEntries)
if (condMet) enabledButtons.add(button)
if (condMet || isCompleted) enabledButtons.add(button)
}
withContext(Dispatchers.Main) {

View File

@ -0,0 +1,96 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.appcompat.widget.Toolbar
android:id="@+id/devSettingsToolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:title="A/B testing (dev)"
app:titleTextColor="@android:color/white" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="8dp"
android:text="Manual A/B override for local testing. Not a security boundary."
android:textSize="14sp" />
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/switchAbOverride"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingVertical="8dp"
android:text="Manual A/B override" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="16dp"
android:paddingBottom="8dp"
android:text="Variant"
android:textStyle="bold" />
<RadioGroup
android:id="@+id/radioGroupVariant"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<RadioButton
android:id="@+id/radioVariantDefault"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Default (no override)" />
<RadioButton
android:id="@+id/radioVariantA"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Variant A" />
<RadioButton
android:id="@+id/radioVariantB"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Variant B" />
</RadioGroup>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="24dp"
android:layout_marginBottom="16dp"
android:background="#DDDDDD" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="8dp"
android:text="Database"
android:textStyle="bold" />
<Button
android:id="@+id/databaseButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Datenbank" />
</LinearLayout>
</ScrollView>
</LinearLayout>

View File

@ -195,21 +195,6 @@
app:cornerRadius="@dimen/pill_radius"
app:backgroundTint="@color/brand_purple"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/editButton"
android:layout_width="0dp"
android:layout_height="@dimen/pill_height"
android:layout_weight="1"
android:layout_marginEnd="12dp"
android:textAllCaps="false"
android:textColor="@android:color/white"
app:icon="@drawable/ic_dot_16"
app:iconTint="@android:color/white"
app:iconPadding="8dp"
app:iconGravity="textStart"
app:cornerRadius="@dimen/pill_radius"
app:backgroundTint="@color/brand_purple"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/saveButton"
android:layout_width="0dp"
@ -253,19 +238,6 @@
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="match_parent"
android:layout_height="@dimen/pill_height"
android:layout_marginBottom="12dp"
android:textAllCaps="false"
android:textColor="@color/brand_purple"
app:cornerRadius="@dimen/pill_radius"
app:strokeColor="@color/brand_purple"
app:strokeWidth="@dimen/pill_stroke"
app:backgroundTint="@android:color/transparent"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/databaseButton"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="match_parent"
android:layout_height="@dimen/pill_height"
android:textAllCaps="false"
android:textColor="@color/brand_purple"
app:cornerRadius="@dimen/pill_radius"

View File

@ -0,0 +1,71 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.button.MaterialButton
android:id="@+id/btnBack"
android:layout_width="@dimen/nav_btn_size"
android:layout_height="@dimen/nav_btn_size"
android:layout_marginStart="12dp"
android:layout_marginTop="12dp"
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"
app:iconPadding="0dp"
app:cornerRadius="999dp"
app:backgroundTint="@color/btn_nav_left_tint"
app:rippleColor="@color/btn_nav_left_ripple" />
<androidx.core.widget.NestedScrollView
android:id="@+id/scrollContainer"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:fillViewport="true"
android:clipToPadding="false"
android:paddingBottom="16dp">
<LinearLayout
android:id="@+id/questionContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:paddingTop="8dp" />
</androidx.core.widget.NestedScrollView>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="12dp"
android:background="@android:color/white"
android:elevation="8dp">
<com.google.android.material.button.MaterialButton
android:id="@+id/btnSave"
android:layout_width="match_parent"
android:layout_height="@dimen/nav_btn_size"
android:textAllCaps="false"
android:minWidth="0dp"
android:insetLeft="0dp"
android:insetRight="0dp"
app:cornerRadius="999dp"
app:backgroundTint="@color/btn_nav_right_tint"
app:rippleColor="@color/btn_nav_right_ripple" />
<ProgressBar
android:id="@+id/progressBar"
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone" />
</FrameLayout>
</LinearLayout>