added session time stamp and online/offline state

This commit is contained in:
oxidiert
2025-09-26 12:28:11 +02:00
parent bf33501b69
commit cfcb689ffc
7 changed files with 255 additions and 182 deletions

View File

@ -3,7 +3,9 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
package="com.dano.test1"> package="com.dano.test1">
<!-- Netzwerkberechtigungen -->
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application <application
android:name=".MyApp" android:name=".MyApp"
@ -16,6 +18,7 @@
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.Test1" android:theme="@style/Theme.Test1"
android:networkSecurityConfig="@xml/network_security_config" android:networkSecurityConfig="@xml/network_security_config"
android:usesCleartextTraffic="true"
tools:targetApi="31"> tools:targetApi="31">
<provider <provider

View File

@ -2,6 +2,8 @@ package com.dano.test1
import android.content.res.ColorStateList import android.content.res.ColorStateList
import android.graphics.Color import android.graphics.Color
import android.os.Handler
import android.os.Looper
import android.util.TypedValue import android.util.TypedValue
import android.view.Gravity import android.view.Gravity
import android.view.View import android.view.View
@ -10,6 +12,7 @@ import com.google.android.material.button.MaterialButton
import org.json.JSONArray import org.json.JSONArray
import org.json.JSONObject import org.json.JSONObject
import java.io.File import java.io.File
import java.util.concurrent.TimeUnit
var RHS_POINTS: Int? = null var RHS_POINTS: Int? = null
@ -27,6 +30,10 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
private lateinit var downloadButton: Button private lateinit var downloadButton: Button
private lateinit var databaseButton: Button private lateinit var databaseButton: Button
// NEU: Status-Leiste
private lateinit var statusSession: TextView
private lateinit var statusOnline: TextView
private val dynamicButtons = mutableListOf<Button>() private val dynamicButtons = mutableListOf<Button>()
private val questionnaireFiles = mutableMapOf<Button, String>() private val questionnaireFiles = mutableMapOf<Button, String>()
@ -42,6 +49,15 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
private var uiFreeze: Boolean = false private var uiFreeze: Boolean = false
// Status-Updater (jede Minute)
private val uiHandler = Handler(Looper.getMainLooper())
private val statusTicker = object : Runnable {
override fun run() {
updateStatusStrip()
uiHandler.postDelayed(this, 60_000) // jede Minute
}
}
// Feste Standard-Randfarben // Feste Standard-Randfarben
private val STROKE_ENABLED = Color.parseColor("#8C79F2") // wenn anklickbar private val STROKE_ENABLED = Color.parseColor("#8C79F2") // wenn anklickbar
private val STROKE_DISABLED = Color.parseColor("#D8D3F5") // wenn nicht anklickbar private val STROKE_DISABLED = Color.parseColor("#D8D3F5") // wenn nicht anklickbar
@ -62,6 +78,11 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
setupDownloadButton() setupDownloadButton()
setupDatabaseButtonHandler() setupDatabaseButtonHandler()
// Statusleiste initial & Ticker starten
uiHandler.removeCallbacks(statusTicker)
updateStatusStrip()
uiHandler.post(statusTicker)
val pathExists = File("/data/data/com.dano.test1/databases/questionnaire_database").exists() val pathExists = File("/data/data/com.dano.test1/databases/questionnaire_database").exists()
updateMainButtonsState(pathExists) updateMainButtonsState(pathExists)
if (pathExists && !editText.text.isNullOrBlank()) buttonLoad.performClick() if (pathExists && !editText.text.isNullOrBlank()) buttonLoad.performClick()
@ -79,6 +100,10 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
downloadButton = activity.findViewById(R.id.downloadButton) downloadButton = activity.findViewById(R.id.downloadButton)
databaseButton = activity.findViewById(R.id.databaseButton) databaseButton = activity.findViewById(R.id.databaseButton)
// NEU:
statusSession = activity.findViewById(R.id.statusSession)
statusOnline = activity.findViewById(R.id.statusOnline)
val tag = editText.tag as? String ?: "" val tag = editText.tag as? String ?: ""
editText.hint = t(tag) editText.hint = t(tag)
textView.text = t("example_text") textView.text = t("example_text")
@ -278,6 +303,9 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
private fun applyUpdateButtonTexts(force: Boolean) { private fun applyUpdateButtonTexts(force: Boolean) {
if (uiFreeze && !force) return if (uiFreeze && !force) return
// Statuszeile bei jedem Refresh aktualisieren
updateStatusStrip()
questionnaireFiles.forEach { (button, fileName) -> questionnaireFiles.forEach { (button, fileName) ->
val entry = questionnaireEntries.firstOrNull { it.file == fileName } val entry = questionnaireEntries.firstOrNull { it.file == fileName }
val key = fileName.substringAfter("questionnaire_").substringAfter("_").removeSuffix(".json") val key = fileName.substringAfter("questionnaire_").substringAfter("_").removeSuffix(".json")
@ -291,7 +319,6 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
val enabled = button.isEnabled val enabled = button.isEnabled
val locked = !enabled && !completed val locked = !enabled && !completed
// Rahmenbreite & -farbe je nach Klickbarkeit
setClickableStroke(button, enabled) setClickableStroke(button, enabled)
if (locked) { if (locked) {
@ -302,7 +329,6 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
setLockedAppearance(button, false) setLockedAppearance(button, false)
parts.title.setTextColor(Color.parseColor("#2F2A49")) parts.title.setTextColor(Color.parseColor("#2F2A49"))
parts.subtitle.setTextColor(Color.parseColor("#7B7794")) parts.subtitle.setTextColor(Color.parseColor("#7B7794"))
// Tönung nur wenn showPoints == true
applyTintForButton(button, points, emphasize = enabled) applyTintForButton(button, points, emphasize = enabled)
} }
@ -357,7 +383,6 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
button.isEnabled = shouldEnable button.isEnabled = shouldEnable
button.alpha = if (completed || shouldEnable) 1.0f else 0.6f button.alpha = if (completed || shouldEnable) 1.0f else 0.6f
// Rahmenbreite & -farbe je nach Klickbarkeit
setClickableStroke(button, shouldEnable) setClickableStroke(button, shouldEnable)
cardParts[button]?.let { parts -> cardParts[button]?.let { parts ->
@ -369,7 +394,6 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
setLockedAppearance(button, false) setLockedAppearance(button, false)
parts.title.setTextColor(Color.parseColor("#2F2A49")) parts.title.setTextColor(Color.parseColor("#2F2A49"))
parts.subtitle.setTextColor(Color.parseColor("#7B7794")) parts.subtitle.setTextColor(Color.parseColor("#7B7794"))
// Tönung nur wenn showPoints == true
applyTintForButton(button, getPointsForButton(button), emphasize = shouldEnable) applyTintForButton(button, getPointsForButton(button), emphasize = shouldEnable)
} }
@ -394,11 +418,6 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
} }
} }
// öffentliche Wrapper
private fun updateButtonTexts() = applyUpdateButtonTexts(force = false)
private fun setButtonsEnabled(enabledButtons: List<Button>, allowCompleted: Boolean = false) =
applySetButtonsEnabled(enabledButtons, allowCompleted, force = false)
private fun startQuestionnaire(questionnaire: QuestionnaireBase<*>) { private fun startQuestionnaire(questionnaire: QuestionnaireBase<*>) {
activity.startQuestionnaire(questionnaire, languageID) activity.startQuestionnaire(questionnaire, languageID)
} }
@ -434,88 +453,23 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
private fun setupUploadButton() { private fun setupUploadButton() {
uploadButton.text = t("upload") uploadButton.text = t("upload")
uploadButton.setOnClickListener { uploadButton.setOnClickListener {
// Token aus dem Login (MainActivity speichert es in TokenStore)
val token = TokenStore.getToken(activity) val token = TokenStore.getToken(activity)
if (token.isNullOrBlank()) { if (token.isNullOrBlank()) {
Toast.makeText( Toast.makeText(activity, t("login_required") ?: "Bitte zuerst einloggen", Toast.LENGTH_LONG).show()
activity,
t("login_required") ?: "Bitte zuerst einloggen",
Toast.LENGTH_LONG
).show()
return@setOnClickListener return@setOnClickListener
} }
// wie gewohnt: Client-Code merken, dann direkt hochladen
GlobalValues.LAST_CLIENT_CODE = editText.text.toString().trim() GlobalValues.LAST_CLIENT_CODE = editText.text.toString().trim()
DatabaseUploader.uploadDatabaseWithToken(activity, token) DatabaseUploader.uploadDatabaseWithToken(activity, token)
// Hinweis: Nach erfolgreichem Upload schließt sich die App (exitProcess(0)) wie bisher.
} }
} }
private fun setupDownloadButton() { private fun setupDownloadButton() {
downloadButton.text = t("download") downloadButton.text = t("download")
downloadButton.setOnClickListener { downloadButton.setOnClickListener {
GlobalValues.LAST_CLIENT_CODE = editText.text.toString().trim() Toast.makeText(activity, t("login_required") ?: "Bitte zuerst einloggen", Toast.LENGTH_SHORT).show()
promptCredentials(
title = t("login_required") ?: "Login erforderlich",
onOk = { user, pass ->
// Login -> Token -> Download
LoginManager.loginUserWithCredentials(
context = activity,
username = user,
password = pass,
onSuccess = { token ->
Toast.makeText(activity, t("login_ok") ?: "Login OK", Toast.LENGTH_SHORT).show()
DatabaseDownloader.downloadAndReplaceDatabase(activity, token)
updateMainButtonsState(true)
},
onError = { error ->
Toast.makeText(activity, error, Toast.LENGTH_LONG).show()
}
)
}
)
} }
} }
private fun promptCredentials(
title: String,
onOk: (username: String, password: String) -> Unit
) {
val wrapper = LinearLayout(activity).apply {
orientation = LinearLayout.VERTICAL
setPadding(dp(20), dp(8), dp(20), 0)
}
val etUser = EditText(activity).apply {
hint = "Username"
setSingleLine()
}
val etPass = EditText(activity).apply {
hint = "Passwort"
setSingleLine()
inputType = android.text.InputType.TYPE_CLASS_TEXT or
android.text.InputType.TYPE_TEXT_VARIATION_PASSWORD
}
wrapper.addView(etUser)
wrapper.addView(etPass)
android.app.AlertDialog.Builder(activity)
.setTitle(title)
.setView(wrapper)
.setPositiveButton("OK") { _, _ ->
val u = etUser.text.toString().trim()
val p = etPass.text.toString()
if (u.isNotEmpty() && p.isNotEmpty()) {
onOk(u, p)
} else {
Toast.makeText(activity, t("enter_password") ?: "Bitte Username & Passwort eingeben", Toast.LENGTH_SHORT).show()
}
}
.setNegativeButton(t("cancel") ?: "Abbrechen", null)
.show()
}
private fun setupDatabaseButtonHandler() { private fun setupDatabaseButtonHandler() {
DatabaseButtonHandler( DatabaseButtonHandler(
activity = activity, activity = activity,
@ -532,8 +486,7 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
} }
} }
private fun dp(v: Int): Int = private fun dp(v: Int): Int = (v * activity.resources.displayMetrics.density).toInt()
(v * activity.resources.displayMetrics.density).toInt()
private fun isCompleted(button: Button): Boolean { private fun isCompleted(button: Button): Boolean {
val fileName = questionnaireFiles[button] ?: return false val fileName = questionnaireFiles[button] ?: return false
@ -549,7 +502,6 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
}?.value }?.value
} }
/** Locked-Optik: schwarzer Hintergrund, fester grauer Rand */
private fun setLockedAppearance(button: Button, locked: Boolean) { private fun setLockedAppearance(button: Button, locked: Boolean) {
val mb = button as? MaterialButton ?: return val mb = button as? MaterialButton ?: return
if (locked) { if (locked) {
@ -561,14 +513,12 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
} }
} }
/** Standardfarben wiederherstellen (wenn showPoints == false) hier nicht mehr für locked zuständig */
private fun resetTint(button: Button) { private fun resetTint(button: Button) {
val mb = button as? MaterialButton ?: return val mb = button as? MaterialButton ?: return
mb.backgroundTintList = ColorStateList.valueOf(Color.WHITE) mb.backgroundTintList = ColorStateList.valueOf(Color.WHITE)
mb.strokeColor = ColorStateList.valueOf(STROKE_DISABLED) mb.strokeColor = ColorStateList.valueOf(STROKE_DISABLED)
} }
/** Tönung nur anwenden, wenn showPoints == true (und Button nicht locked) */
private fun applyTintForButton(button: Button, points: Int?, emphasize: Boolean) { private fun applyTintForButton(button: Button, points: Int?, emphasize: Boolean) {
val file = questionnaireFiles[button] ?: return val file = questionnaireFiles[button] ?: return
val entry = questionnaireEntries.firstOrNull { it.file == file } val entry = questionnaireEntries.firstOrNull { it.file == file }
@ -579,21 +529,17 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
mb.strokeColor = ColorStateList.valueOf(if (emphasize) STROKE_ENABLED else STROKE_DISABLED) mb.strokeColor = ColorStateList.valueOf(if (emphasize) STROKE_ENABLED else STROKE_DISABLED)
return return
} }
setScoreTint(button, points, emphasize) setScoreTint(button, points, emphasize)
} }
/** Dickere Kontur, wenn anklickbar; Farbe bleibt fix */
private fun setClickableStroke(button: Button, enabled: Boolean) { private fun setClickableStroke(button: Button, enabled: Boolean) {
val mb = button as? MaterialButton ?: return val mb = button as? MaterialButton ?: return
mb.strokeWidth = if (enabled) dp(2) else dp(1) mb.strokeWidth = if (enabled) dp(2) else dp(1)
mb.strokeColor = ColorStateList.valueOf(if (enabled) STROKE_ENABLED else STROKE_DISABLED) mb.strokeColor = ColorStateList.valueOf(if (enabled) STROKE_ENABLED else STROKE_DISABLED)
} }
/** Hintergrund nach Score; Rand bleibt fix */
private fun setScoreTint(button: Button, points: Int?, emphasize: Boolean) { private fun setScoreTint(button: Button, points: Int?, emphasize: Boolean) {
val mb = button as? MaterialButton ?: return val mb = button as? MaterialButton ?: return
val bg = when { val bg = when {
points == null && emphasize -> Color.parseColor("#F1EEFF") points == null && emphasize -> Color.parseColor("#F1EEFF")
points == null -> Color.parseColor("#FFFFFF") points == null -> Color.parseColor("#FFFFFF")
@ -604,8 +550,27 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
points >= 37 && emphasize -> Color.parseColor("#FFCDD2") points >= 37 && emphasize -> Color.parseColor("#FFCDD2")
else -> Color.parseColor("#FFEBEE") else -> Color.parseColor("#FFEBEE")
} }
mb.backgroundTintList = ColorStateList.valueOf(bg) mb.backgroundTintList = ColorStateList.valueOf(bg)
mb.strokeColor = ColorStateList.valueOf(if (emphasize) STROKE_ENABLED else STROKE_DISABLED) mb.strokeColor = ColorStateList.valueOf(if (emphasize) STROKE_ENABLED else STROKE_DISABLED)
} }
// === NEU: Status-Logik ===
private fun updateStatusStrip() {
// Session-Alter
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: —"
// Online/Offline
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() {
// nutzt deine bestehende Update-Logik und erzwingt ein Neuzeichnen
applyUpdateButtonTexts(force = true)
}
} }

View File

@ -16,7 +16,6 @@ object LoginManager {
private const val SERVER_LOGIN_URL = "http://49.13.157.44/login.php" private const val SERVER_LOGIN_URL = "http://49.13.157.44/login.php"
private val client = OkHttpClient() private val client = OkHttpClient()
// Neuer Login mit Username+Passwort
fun loginUserWithCredentials( fun loginUserWithCredentials(
context: Context, context: Context,
username: String, username: String,
@ -26,14 +25,15 @@ object LoginManager {
) { ) {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
try { try {
val bodyJson = JSONObject().apply { val bodyJson = JSONObject()
put("username", username) .put("username", username)
put("password", password) .put("password", password)
}.toString() .toString()
.toRequestBody("application/json".toMediaType())
val request = Request.Builder() val request = Request.Builder()
.url(SERVER_LOGIN_URL) .url(SERVER_LOGIN_URL)
.post(bodyJson.toRequestBody("application/json".toMediaType())) .post(bodyJson)
.build() .build()
val response = client.newCall(request).execute() val response = client.newCall(request).execute()
@ -43,9 +43,11 @@ object LoginManager {
val json = JSONObject(text) val json = JSONObject(text)
if (json.optBoolean("success")) { if (json.optBoolean("success")) {
val token = json.getString("token") val token = json.getString("token")
// => setzt auch den Login-Timestamp:
TokenStore.save(context, token, username)
withContext(Dispatchers.Main) { onSuccess(token) } withContext(Dispatchers.Main) { onSuccess(token) }
} else { } else {
withContext(Dispatchers.Main) { onError(json.optString("message", "Login fehlgeschlagen")) } withContext(Dispatchers.Main) { onError("Login fehlgeschlagen") }
} }
} else { } else {
withContext(Dispatchers.Main) { onError("Fehler beim Login (${response.code})") } withContext(Dispatchers.Main) { onError("Fehler beim Login (${response.code})") }
@ -56,15 +58,4 @@ object LoginManager {
} }
} }
} }
// Alte Signatur (nur Passwort) für evtl. Altcode beibehalten
fun loginUser(
context: Context,
password: String,
onSuccess: (String) -> Unit,
onError: (String) -> Unit
) {
// nicht mehr genutzt; leitet auf neuen Login um mit leerem User
loginUserWithCredentials(context, "", password, onSuccess, onError)
}
} }

View File

@ -1,6 +1,10 @@
package com.dano.test1 package com.dano.test1
import android.content.res.Configuration import android.content.res.Configuration
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import android.widget.EditText import android.widget.EditText
@ -19,15 +23,16 @@ class MainActivity : AppCompatActivity() {
private var progress: ProgressBar? = null private var progress: ProgressBar? = null
// LIVE: Network-Callback (wird in onResume registriert, in onPause deregistriert)
private var netCb: ConnectivityManager.NetworkCallback? = null
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
// Login-Dialog -> Login -> Auto-Download -> OpeningScreen
// Zeige sofort Login-Dialog; erst NACH erfolgreichem Login + Download initialisieren wir den Opening Screen
showLoginThenDownload() showLoginThenDownload()
} }
private fun showLoginThenDownload() { private fun showLoginThenDownload() {
// UI mit Username/Passwort
val container = LinearLayout(this).apply { val container = LinearLayout(this).apply {
orientation = LinearLayout.VERTICAL orientation = LinearLayout.VERTICAL
setPadding(dp(20), dp(8), dp(20), 0) setPadding(dp(20), dp(8), dp(20), 0)
@ -42,8 +47,7 @@ class MainActivity : AppCompatActivity() {
inputType = android.text.InputType.TYPE_CLASS_TEXT or inputType = android.text.InputType.TYPE_CLASS_TEXT or
android.text.InputType.TYPE_TEXT_VARIATION_PASSWORD android.text.InputType.TYPE_TEXT_VARIATION_PASSWORD
} }
container.addView(etUser) container.addView(etUser); container.addView(etPass)
container.addView(etPass)
val dialog = AlertDialog.Builder(this) val dialog = AlertDialog.Builder(this)
.setTitle("Login erforderlich") .setTitle("Login erforderlich")
@ -58,14 +62,12 @@ class MainActivity : AppCompatActivity() {
return@setPositiveButton return@setPositiveButton
} }
showBusy(true) showBusy(true)
// Login -> Token -> Auto-Download -> OpeningScreen init
LoginManager.loginUserWithCredentials( LoginManager.loginUserWithCredentials(
context = this, context = this,
username = user, username = user,
password = pass, password = pass,
onSuccess = { token -> onSuccess = { token ->
// optional speichern // Token & Login-Timestamp werden in TokenStore gespeichert (siehe LoginManager/TokenStore).
TokenStore.save(this, user, token)
DatabaseDownloader.downloadAndReplaceDatabase( DatabaseDownloader.downloadAndReplaceDatabase(
context = this, context = this,
token = token token = token
@ -74,22 +76,20 @@ class MainActivity : AppCompatActivity() {
if (!ok) { if (!ok) {
Toast.makeText(this, "Download fehlgeschlagen", Toast.LENGTH_LONG).show() Toast.makeText(this, "Download fehlgeschlagen", Toast.LENGTH_LONG).show()
} }
// Jetzt erst die Start-UI hochziehen
openingScreenHandler = HandlerOpeningScreen(this) openingScreenHandler = HandlerOpeningScreen(this)
openingScreenHandler.init() openingScreenHandler.init()
// Ersten Status sofort anzeigen
openingScreenHandler.refreshHeaderStatusLive()
} }
}, },
onError = { msg -> onError = { msg ->
showBusy(false) showBusy(false)
Toast.makeText(this, msg, Toast.LENGTH_LONG).show() Toast.makeText(this, msg, Toast.LENGTH_LONG).show()
// erneut anbieten
showLoginThenDownload() showLoginThenDownload()
} }
) )
} }
.setNegativeButton("Beenden") { _, _ -> .setNegativeButton("Beenden") { _, _ -> finishAffinity() }
finishAffinity()
}
.create() .create()
dialog.show() dialog.show()
@ -100,14 +100,7 @@ class MainActivity : AppCompatActivity() {
if (progress == null) { if (progress == null) {
progress = ProgressBar(this).apply { progress = ProgressBar(this).apply {
isIndeterminate = true isIndeterminate = true
// simpler Vollbild-Overlay (window?.decorView as? android.view.ViewGroup)?.addView(this)
val content = window?.decorView as? android.view.ViewGroup
content?.addView(this, android.view.ViewGroup.LayoutParams(
android.view.ViewGroup.LayoutParams.WRAP_CONTENT,
android.view.ViewGroup.LayoutParams.WRAP_CONTENT
))
this.x = (resources.displayMetrics.widthPixels / 2f) - width / 2f
this.y = (resources.displayMetrics.heightPixels / 2f) - height / 2f
} }
} }
progress?.visibility = View.VISIBLE progress?.visibility = View.VISIBLE
@ -118,7 +111,62 @@ class MainActivity : AppCompatActivity() {
private fun dp(v: Int): Int = (v * resources.displayMetrics.density).toInt() private fun dp(v: Int): Int = (v * resources.displayMetrics.density).toInt()
// Wichtig: Keine Neu-Initialisierung bei Rotation // --- LIVE NETZSTATUS ---
override fun onResume() {
super.onResume()
registerNetworkCallback()
// Falls die Startseite schon steht: Status jetzt gleich ziehen
if (::openingScreenHandler.isInitialized && !isInQuestionnaire) {
openingScreenHandler.refreshHeaderStatusLive()
}
}
override fun onPause() {
super.onPause()
unregisterNetworkCallback()
}
private fun registerNetworkCallback() {
if (netCb != null) return // schon aktiv
val cm = getSystemService(ConnectivityManager::class.java)
val req = NetworkRequest.Builder()
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.build()
netCb = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
runOnUiThread {
if (::openingScreenHandler.isInitialized && !isInQuestionnaire) {
openingScreenHandler.refreshHeaderStatusLive()
}
}
}
override fun onLost(network: Network) {
runOnUiThread {
if (::openingScreenHandler.isInitialized && !isInQuestionnaire) {
openingScreenHandler.refreshHeaderStatusLive()
}
}
}
// Optional: auch auf „nicht validiert“ reagieren
override fun onCapabilitiesChanged(network: Network, caps: NetworkCapabilities) {
runOnUiThread {
if (::openingScreenHandler.isInitialized && !isInQuestionnaire) {
openingScreenHandler.refreshHeaderStatusLive()
}
}
}
}
cm.registerNetworkCallback(req, netCb!!)
}
private fun unregisterNetworkCallback() {
val cb = netCb ?: return
val cm = getSystemService(ConnectivityManager::class.java)
cm.unregisterNetworkCallback(cb)
netCb = null
}
// --- /LIVE NETZSTATUS ---
override fun onConfigurationChanged(newConfig: Configuration) { override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig) super.onConfigurationChanged(newConfig)
} }

View File

@ -0,0 +1,19 @@
package com.dano.test1
import android.content.Context
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
object NetworkUtils {
fun isOnline(context: Context): Boolean {
return try {
val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager? ?: return false
val network = cm.activeNetwork ?: return false
val caps = cm.getNetworkCapabilities(network) ?: return false
caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) &&
caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
} catch (_: SecurityException) {
false
}
}
}

View File

@ -1,24 +1,34 @@
package com.dano.test1 package com.dano.test1
import android.content.Context import android.content.Context
import android.content.SharedPreferences
object TokenStore { object TokenStore {
private const val PREF = "auth_prefs" private const val PREF = "qdb_prefs"
private const val KEY_TOKEN = "token" private const val KEY_TOKEN = "token"
private const val KEY_USER = "username" private const val KEY_USER = "user"
private const val KEY_LOGIN_TS = "login_ts"
fun save(context: Context, username: String, token: String) { /** Speichert Token, Username und Login-Timestamp (jetzt) */
prefs(context).edit() 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_TOKEN, token)
.putString(KEY_USER, username) .putString(KEY_USER, username)
.putLong(KEY_LOGIN_TS, now)
.apply() .apply()
} }
fun getToken(context: Context): String? = prefs(context).getString(KEY_TOKEN, null) fun getToken(context: Context): String? =
fun getUsername(context: Context): String? = prefs(context).getString(KEY_USER, null) context.getSharedPreferences(PREF, Context.MODE_PRIVATE).getString(KEY_TOKEN, null)
fun clear(context: Context) { prefs(context).edit().clear().apply() }
private fun prefs(ctx: Context): SharedPreferences = fun getUsername(context: Context): String? =
ctx.getSharedPreferences(PREF, Context.MODE_PRIVATE) context.getSharedPreferences(PREF, Context.MODE_PRIVATE).getString(KEY_USER, null)
fun getLoginTimestamp(context: Context): Long =
context.getSharedPreferences(PREF, Context.MODE_PRIVATE).getLong(KEY_LOGIN_TS, 0L)
fun clear(context: Context) {
context.getSharedPreferences(PREF, Context.MODE_PRIVATE).edit().clear().apply()
}
} }

View File

@ -23,70 +23,107 @@
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"> app:layout_constraintEnd_toEndOf="parent">
<!-- Neu: vertikale Anordnung -> Eingaben + Statuszeile -->
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal" android:orientation="vertical">
android:gravity="center_vertical"
android:baselineAligned="false"
android:paddingBottom="6dp">
<!-- Zeile mit Sprache + Client-Code -->
<LinearLayout <LinearLayout
android:layout_width="0dp" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:orientation="horizontal"
android:orientation="vertical" android:gravity="center_vertical"
android:layout_marginEnd="8dp"> android:baselineAligned="false"
android:paddingBottom="6dp">
<TextView <LinearLayout
android:layout_width="wrap_content" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="Language" android:layout_weight="1"
android:textColor="@color/brand_text_dark" android:orientation="vertical"
android:textSize="12sp" android:layout_marginEnd="8dp">
android:paddingStart="4dp"
android:paddingBottom="6dp"/>
<Spinner <TextView
android:id="@+id/string_spinner1" android:layout_width="wrap_content"
android:layout_width="match_parent" android:layout_height="wrap_content"
android:layout_height="48dp" android:text="Language"
android:layout_marginStart="8dp" android:textColor="@color/brand_text_dark"
android:layout_marginEnd="8dp" android:textSize="12sp"
android:background="@drawable/bg_field_filled" android:paddingStart="4dp"
android:paddingStart="12dp" android:paddingBottom="6dp"/>
android:paddingEnd="12dp"/>
<Spinner
android:id="@+id/string_spinner1"
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:background="@drawable/bg_field_filled"
android:paddingStart="12dp"
android:paddingEnd="12dp"/>
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="2"
android:orientation="vertical"
android:layout_marginStart="8dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="4dp"
android:paddingBottom="6dp"
android:text="Client Code"
android:textColor="@color/brand_text_dark"
android:textSize="12sp" />
<EditText
android:id="@+id/editText"
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:background="@drawable/bg_field_filled"
android:ems="10"
android:inputType="text"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:tag="client_code"/>
</LinearLayout>
</LinearLayout> </LinearLayout>
<!-- NEU: Statuszeile (Session-Alter & Online/Offline) -->
<LinearLayout <LinearLayout
android:layout_width="0dp" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="2" android:orientation="horizontal"
android:orientation="vertical" android:gravity="center_vertical"
android:layout_marginStart="8dp"> android:paddingTop="6dp">
<TextView <TextView
android:id="@+id/statusSession"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Session: —"
android:textColor="@color/brand_text_dark"
android:textSize="13sp" />
<TextView
android:id="@+id/statusOnline"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingStart="4dp" android:text="Offline"
android:paddingBottom="6dp" android:textStyle="bold"
android:text="Client Code" android:textSize="13sp"
android:textColor="@color/brand_text_dark" android:textColor="#C62828"
android:textSize="12sp" /> android:paddingStart="12dp"/>
<EditText
android:id="@+id/editText"
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:background="@drawable/bg_field_filled"
android:ems="10"
android:inputType="text"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:tag="client_code"/>
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>
</com.google.android.material.card.MaterialCardView> </com.google.android.material.card.MaterialCardView>