added session time stamp and online/offline state
This commit is contained in:
@ -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
|
||||||
|
|||||||
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
19
app/src/main/java/com/dano/test1/NetworkUtilis.kt
Normal file
19
app/src/main/java/com/dano/test1/NetworkUtilis.kt
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user