change aes, now more secure, beacuse no hardcode anymore
This commit is contained in:
@ -1,56 +1,73 @@
|
||||
// app/src/main/java/com/dano/test1/AES256Helper.kt
|
||||
package com.dano.test1
|
||||
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileOutputStream
|
||||
import java.security.SecureRandom
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.CipherInputStream
|
||||
import javax.crypto.CipherOutputStream
|
||||
import javax.crypto.Mac
|
||||
import javax.crypto.spec.IvParameterSpec
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
import kotlin.random.Random
|
||||
import kotlin.math.min
|
||||
|
||||
object AES256Helper {
|
||||
|
||||
private const val TRANSFORMATION = "AES/CBC/PKCS5Padding"
|
||||
private const val ALGORITHM = "AES"
|
||||
private const val IV_SIZE = 16
|
||||
// HKDF-SHA256: IKM = tokenHex->bytes, salt="", info="qdb-aes", len=32
|
||||
private fun hkdfFromToken(tokenHex: String, info: String = "qdb-aes", len: Int = 32): ByteArray {
|
||||
val ikm = hexToBytes(tokenHex)
|
||||
val mac = Mac.getInstance("HmacSHA256")
|
||||
val zeroSalt = ByteArray(32) { 0 }
|
||||
mac.init(SecretKeySpec(zeroSalt, "HmacSHA256"))
|
||||
val prk = mac.doFinal(ikm)
|
||||
|
||||
// Beispiel-Key: 32 Bytes = 256 bit. Ersetze das durch deinen eigenen sicheren Schlüssel!
|
||||
private val keyBytes = "12345678901234567890123456789012".toByteArray(Charsets.UTF_8)
|
||||
private val secretKey = SecretKeySpec(keyBytes, ALGORITHM)
|
||||
|
||||
// Verschlüsseln: InputFile -> OutputFile (mit zufälligem IV vorne in der Datei)
|
||||
fun encryptFile(inputFile: File, outputFile: File) {
|
||||
val iv = ByteArray(IV_SIZE)
|
||||
Random.nextBytes(iv)
|
||||
val ivSpec = IvParameterSpec(iv)
|
||||
val cipher = Cipher.getInstance(TRANSFORMATION)
|
||||
cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivSpec)
|
||||
|
||||
FileOutputStream(outputFile).use { fileOut ->
|
||||
// IV vorne reinschreiben
|
||||
fileOut.write(iv)
|
||||
CipherOutputStream(fileOut, cipher).use { cipherOut ->
|
||||
FileInputStream(inputFile).use { fileIn ->
|
||||
fileIn.copyTo(cipherOut)
|
||||
}
|
||||
}
|
||||
var previous = ByteArray(0)
|
||||
val okm = ByteArray(len)
|
||||
var generated = 0
|
||||
var counter = 1
|
||||
while (generated < len) {
|
||||
mac.init(SecretKeySpec(prk, "HmacSHA256"))
|
||||
mac.update(previous)
|
||||
mac.update(info.toByteArray(Charsets.UTF_8))
|
||||
mac.update(counter.toByte())
|
||||
val t = mac.doFinal()
|
||||
val toCopy = min(len - generated, t.size)
|
||||
System.arraycopy(t, 0, okm, generated, toCopy)
|
||||
previous = t
|
||||
generated += toCopy
|
||||
counter++
|
||||
}
|
||||
return okm
|
||||
}
|
||||
|
||||
// Entschlüsseln: InputFile (IV+Ciphertext) -> OutputFile (Klartext)
|
||||
fun decryptFile(inputFile: File, outputFile: File) {
|
||||
FileInputStream(inputFile).use { fileIn ->
|
||||
val iv = ByteArray(IV_SIZE)
|
||||
if (fileIn.read(iv) != IV_SIZE) throw IllegalArgumentException("Ungültige Datei oder IV fehlt")
|
||||
val ivSpec = IvParameterSpec(iv)
|
||||
val cipher = Cipher.getInstance(TRANSFORMATION)
|
||||
cipher.init(Cipher.DECRYPT_MODE, secretKey, ivSpec)
|
||||
|
||||
CipherInputStream(fileIn, cipher).use { cipherIn ->
|
||||
FileOutputStream(outputFile).use { fileOut ->
|
||||
cipherIn.copyTo(fileOut)
|
||||
}
|
||||
}
|
||||
private fun hexToBytes(hex: String): ByteArray {
|
||||
val clean = hex.trim()
|
||||
val len = clean.length
|
||||
val out = ByteArray(len / 2)
|
||||
var i = 0
|
||||
while (i < len) {
|
||||
out[i / 2] = ((Character.digit(clean[i], 16) shl 4) + Character.digit(clean[i + 1], 16)).toByte()
|
||||
i += 2
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
fun encryptFileWithToken(inFile: File, outFile: File, token: String) {
|
||||
val key = hkdfFromToken(token)
|
||||
val iv = ByteArray(16).also { SecureRandom().nextBytes(it) }
|
||||
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
|
||||
cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(iv))
|
||||
val plain = inFile.readBytes()
|
||||
val enc = cipher.doFinal(plain)
|
||||
outFile.writeBytes(iv + enc)
|
||||
}
|
||||
|
||||
fun decryptFileWithToken(inFile: File, token: String): ByteArray {
|
||||
val key = hkdfFromToken(token)
|
||||
val data = inFile.readBytes()
|
||||
require(data.size >= 16) { "cipher too short" }
|
||||
val iv = data.copyOfRange(0, 16)
|
||||
val ct = data.copyOfRange(16, data.size)
|
||||
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
|
||||
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(iv))
|
||||
return cipher.doFinal(ct)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
// app/src/main/java/com/dano/test1/DatabaseDownloader.kt
|
||||
package com.dano.test1
|
||||
|
||||
import android.content.Context
|
||||
@ -9,28 +10,17 @@ import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.spec.IvParameterSpec
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
object DatabaseDownloader {
|
||||
|
||||
private const val DB_NAME = "questionnaire_database"
|
||||
private const val SERVER_DOWNLOAD_URL = "http://49.13.157.44/downloadFull.php"
|
||||
|
||||
// AES-256 Key (muss exakt 32 Bytes lang sein)
|
||||
private const val AES_KEY = "12345678901234567890123456789012"
|
||||
|
||||
private val client = OkHttpClient()
|
||||
|
||||
/**
|
||||
* Startet den Download und Austausch der DB, benötigt gültiges Token
|
||||
*/
|
||||
fun downloadAndReplaceDatabase(context: Context, token: String) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
try {
|
||||
Log.d("DOWNLOAD", "Download gestartet: $SERVER_DOWNLOAD_URL")
|
||||
|
||||
val request = Request.Builder()
|
||||
.url(SERVER_DOWNLOAD_URL)
|
||||
.header("Authorization", "Bearer $token")
|
||||
@ -38,47 +28,24 @@ object DatabaseDownloader {
|
||||
|
||||
val response = client.newCall(request).execute()
|
||||
if (!response.isSuccessful) {
|
||||
Log.e("DOWNLOAD", "Fehler beim Download: ${response.code}")
|
||||
Log.e("DOWNLOAD", "HTTP ${response.code}")
|
||||
return@launch
|
||||
}
|
||||
|
||||
// Zwischenspeichern der verschlüsselten Datei
|
||||
val downloadedFile = File(context.cacheDir, "downloaded_database.enc")
|
||||
val encFile = File(context.cacheDir, "downloaded_database.enc")
|
||||
response.body?.byteStream()?.use { input ->
|
||||
FileOutputStream(downloadedFile).use { output ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
FileOutputStream(encFile).use { output -> input.copyTo(output) }
|
||||
}
|
||||
Log.d("DOWNLOAD", "Datei gespeichert: ${downloadedFile.absolutePath}")
|
||||
|
||||
// Entschlüsselung
|
||||
val decryptedBytes = decryptFile(downloadedFile)
|
||||
val decryptedBytes = AES256Helper.decryptFileWithToken(encFile, token)
|
||||
val dbFile = context.getDatabasePath(DB_NAME)
|
||||
if (dbFile.exists()) dbFile.delete()
|
||||
FileOutputStream(dbFile).use { fos ->
|
||||
fos.write(decryptedBytes)
|
||||
}
|
||||
|
||||
Log.d("DOWNLOAD", "Neue DB erfolgreich entschlüsselt und eingesetzt")
|
||||
FileOutputStream(dbFile).use { it.write(decryptedBytes) }
|
||||
|
||||
Log.d("DOWNLOAD", "DB erfolgreich ersetzt")
|
||||
} catch (e: Exception) {
|
||||
Log.e("DOWNLOAD", "Fehler beim Download oder Ersetzen der DB", e)
|
||||
Log.e("DOWNLOAD", "Fehler", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun decryptFile(file: File): ByteArray {
|
||||
val fileBytes = file.readBytes()
|
||||
if (fileBytes.size < 16) throw IllegalArgumentException("Datei zu kurz, kein IV vorhanden")
|
||||
|
||||
val iv = fileBytes.copyOfRange(0, 16)
|
||||
val cipherBytes = fileBytes.copyOfRange(16, fileBytes.size)
|
||||
|
||||
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
|
||||
val keySpec = SecretKeySpec(AES_KEY.toByteArray(Charsets.UTF_8), "AES")
|
||||
val ivSpec = IvParameterSpec(iv)
|
||||
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec)
|
||||
|
||||
return cipher.doFinal(cipherBytes)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
// app/src/main/java/com/dano/test1/DatabaseUploader.kt
|
||||
package com.dano.test1
|
||||
|
||||
import android.content.Context
|
||||
@ -25,181 +26,111 @@ object DatabaseUploader {
|
||||
|
||||
private val client = OkHttpClient()
|
||||
|
||||
/**
|
||||
* Startet den Upload mit Login über LoginManager.
|
||||
* @param context Android Context
|
||||
* @param password Vom User eingegebenes Passwort
|
||||
*/
|
||||
fun uploadDatabaseWithLogin(context: Context, password: String) {
|
||||
LoginManager.loginUser(context, password,
|
||||
onSuccess = { token ->
|
||||
Log.d("UPLOAD", "Login erfolgreich, Token erhalten")
|
||||
Log.d("UPLOAD", "Login OK")
|
||||
uploadDatabase(context, token)
|
||||
},
|
||||
onError = { errorMsg ->
|
||||
Log.e("UPLOAD", "Login fehlgeschlagen: $errorMsg")
|
||||
}
|
||||
onError = { msg -> Log.e("UPLOAD", "Login fehlgeschlagen: $msg") }
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Interner Upload, benötigt gültiges Token
|
||||
*/
|
||||
private fun uploadDatabase(context: Context, token: String) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
try {
|
||||
val dbFile = context.getDatabasePath(DB_NAME)
|
||||
if (!dbFile.exists()) {
|
||||
Log.e("UPLOAD", "Datenbankdatei existiert nicht: ${dbFile.absolutePath}")
|
||||
Log.e("UPLOAD", "DB fehlt: ${dbFile.absolutePath}")
|
||||
return@launch
|
||||
}
|
||||
|
||||
// WAL-Checkpoint
|
||||
try {
|
||||
val db = SQLiteDatabase.openDatabase(
|
||||
dbFile.absolutePath,
|
||||
null,
|
||||
SQLiteDatabase.OPEN_READWRITE
|
||||
)
|
||||
db.rawQuery("PRAGMA wal_checkpoint(FULL);", null).use { cursor ->
|
||||
if (cursor.moveToFirst()) {
|
||||
try { Log.d("UPLOAD", "WAL-Checkpoint result: ${cursor.getInt(0)}") } catch (_: Exception) {}
|
||||
}
|
||||
}
|
||||
val db = SQLiteDatabase.openDatabase(dbFile.absolutePath, null, SQLiteDatabase.OPEN_READWRITE)
|
||||
db.rawQuery("PRAGMA wal_checkpoint(FULL);", null).use { /* ignore */ }
|
||||
db.close()
|
||||
Log.d("UPLOAD", "WAL-Checkpoint erfolgreich.")
|
||||
} catch (e: Exception) {
|
||||
Log.e("UPLOAD", "Fehler beim WAL-Checkpoint", e)
|
||||
}
|
||||
|
||||
val exists = checkDatabaseExists()
|
||||
if (exists) {
|
||||
Log.d("UPLOAD", "Server-Datenbank vorhanden → Delta-Upload")
|
||||
} else {
|
||||
Log.d("UPLOAD", "Keine Server-Datenbank → Delta-Upload")
|
||||
}
|
||||
} catch (_: Exception) { }
|
||||
|
||||
checkDatabaseExists() // nur Logging
|
||||
uploadPseudoDelta(context, dbFile, token)
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e("UPLOAD", "Fehler beim Hochladen der DB", e)
|
||||
Log.e("UPLOAD", "Fehler", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkDatabaseExists(): Boolean {
|
||||
return try {
|
||||
val request = Request.Builder()
|
||||
.url(SERVER_CHECK_URL)
|
||||
.get()
|
||||
.build()
|
||||
|
||||
client.newCall(request).execute().use { response ->
|
||||
if (!response.isSuccessful) {
|
||||
Log.e("UPLOAD", "checkDatabaseExists HTTP error: ${response.code}")
|
||||
return false
|
||||
}
|
||||
val body = response.body?.string() ?: return false
|
||||
try {
|
||||
val j = JSONObject(body)
|
||||
j.optBoolean("exists", false)
|
||||
} catch (e: Exception) {
|
||||
body.contains("exists", ignoreCase = true)
|
||||
}
|
||||
val req = Request.Builder().url(SERVER_CHECK_URL).get().build()
|
||||
client.newCall(req).execute().use { resp ->
|
||||
if (!resp.isSuccessful) return false
|
||||
val body = resp.body?.string() ?: return false
|
||||
try { JSONObject(body).optBoolean("exists", false) } catch (_: Exception) { false }
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("UPLOAD", "Fehler bei Server-Prüfung", e)
|
||||
false
|
||||
}
|
||||
} catch (e: Exception) { false }
|
||||
}
|
||||
|
||||
private fun uploadPseudoDelta(context: Context, file: File, token: String) {
|
||||
try {
|
||||
val db = SQLiteDatabase.openDatabase(file.absolutePath, null, SQLiteDatabase.OPEN_READONLY)
|
||||
|
||||
val data = JSONObject().apply {
|
||||
put("clients", queryToJsonArray(db, "SELECT clientCode FROM clients"))
|
||||
put("questionnaires", queryToJsonArray(db, "SELECT id FROM questionnaires"))
|
||||
put("questions", queryToJsonArray(db, "SELECT questionId, questionnaireId, question FROM questions"))
|
||||
put("answers", queryToJsonArray(db, "SELECT clientCode, questionId, answerValue FROM answers"))
|
||||
put(
|
||||
"completed_questionnaires",
|
||||
queryToJsonArray(
|
||||
db,
|
||||
"SELECT clientCode, questionnaireId, timestamp, isDone, sumPoints FROM completed_questionnaires"
|
||||
)
|
||||
)
|
||||
put("completed_questionnaires",
|
||||
queryToJsonArray(db, "SELECT clientCode, questionnaireId, timestamp, isDone, sumPoints FROM completed_questionnaires"))
|
||||
}
|
||||
|
||||
db.close()
|
||||
|
||||
val tmpJson = File(context.cacheDir, "payload.json")
|
||||
tmpJson.writeText(data.toString())
|
||||
|
||||
val tmpJson = File(context.cacheDir, "payload.json").apply { writeText(data.toString()) }
|
||||
val tmpEnc = File(context.cacheDir, "payload.enc")
|
||||
try {
|
||||
AES256Helper.encryptFile(tmpJson, tmpEnc)
|
||||
AES256Helper.encryptFileWithToken(tmpJson, tmpEnc, token)
|
||||
} catch (e: Exception) {
|
||||
Log.e("UPLOAD", "Fehler bei der Verschlüsselung der JSON-Datei", e)
|
||||
tmpJson.delete()
|
||||
return
|
||||
Log.e("UPLOAD", "Verschlüsselung fehlgeschlagen", e)
|
||||
tmpJson.delete(); return
|
||||
}
|
||||
|
||||
val requestBody = MultipartBody.Builder()
|
||||
val body = MultipartBody.Builder()
|
||||
.setType(MultipartBody.FORM)
|
||||
.addFormDataPart("token", token) // Token vom Login
|
||||
.addFormDataPart(
|
||||
"file",
|
||||
"payload.enc",
|
||||
tmpEnc.asRequestBody("application/octet-stream".toMediaType())
|
||||
)
|
||||
.build()
|
||||
|
||||
val request = Request.Builder()
|
||||
.url(SERVER_DELTA_URL)
|
||||
.post(requestBody)
|
||||
.addFormDataPart("token", token)
|
||||
.addFormDataPart("file", "payload.enc", tmpEnc.asRequestBody("application/octet-stream".toMediaType()))
|
||||
.build()
|
||||
|
||||
val request = Request.Builder().url(SERVER_DELTA_URL).post(body).build()
|
||||
client.newCall(request).enqueue(object : Callback {
|
||||
override fun onFailure(call: Call, e: IOException) {
|
||||
Log.e("UPLOAD", "Delta-Upload fehlgeschlagen: ${e.message}")
|
||||
tmpJson.delete()
|
||||
tmpEnc.delete()
|
||||
Log.e("UPLOAD", "Fehlgeschlagen: ${e.message}")
|
||||
tmpJson.delete(); tmpEnc.delete()
|
||||
}
|
||||
|
||||
override fun onResponse(call: Call, response: Response) {
|
||||
val body = try { response.body?.string() ?: "Keine Response" } catch (e: Exception) {
|
||||
"Fehler beim Lesen der Response: ${e.message}"
|
||||
}
|
||||
val respBody = try { response.body?.string() ?: "" } catch (_: Exception) { "" }
|
||||
if (response.isSuccessful) {
|
||||
Log.d("UPLOAD", "Delta-Upload erfolgreich: $body")
|
||||
if (file.delete()) Log.d("UPLOAD", "Lokale DB gelöscht.") else Log.e("UPLOAD", "Löschen der lokalen DB fehlgeschlagen.")
|
||||
val journalFile = File(file.parent, file.name + "-journal")
|
||||
if (journalFile.exists() && journalFile.delete()) Log.d("UPLOAD", "Journal-Datei gelöscht.")
|
||||
tmpJson.delete()
|
||||
tmpEnc.delete()
|
||||
exitProcess(0)
|
||||
Log.d("UPLOAD", "OK: $respBody")
|
||||
if (!file.delete()) Log.w("UPLOAD", "Lokale DB nicht gelöscht.")
|
||||
File(file.parent, file.name + "-journal").delete()
|
||||
} else {
|
||||
Log.e("UPLOAD", "Delta-Upload fehlgeschlagen: ${response.code} $body")
|
||||
tmpJson.delete()
|
||||
tmpEnc.delete()
|
||||
Log.e("UPLOAD", "HTTP ${response.code}: $respBody")
|
||||
}
|
||||
tmpJson.delete(); tmpEnc.delete()
|
||||
try { exitProcess(0) } catch (_: Exception) {}
|
||||
}
|
||||
})
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e("UPLOAD", "Fehler beim Delta-Upload", e)
|
||||
Log.e("UPLOAD", "Exception", e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun queryToJsonArray(db: SQLiteDatabase, query: String): JSONArray {
|
||||
val cursor = db.rawQuery(query, null)
|
||||
val jsonArray = JSONArray()
|
||||
cursor.use {
|
||||
val columnNames = it.columnNames
|
||||
val c = db.rawQuery(query, null)
|
||||
val arr = JSONArray()
|
||||
c.use {
|
||||
val cols = it.columnNames
|
||||
while (it.moveToNext()) {
|
||||
val obj = JSONObject()
|
||||
for (col in columnNames) {
|
||||
for (col in cols) {
|
||||
val idx = it.getColumnIndex(col)
|
||||
if (idx >= 0) {
|
||||
when (it.getType(idx)) {
|
||||
@ -207,17 +138,14 @@ object DatabaseUploader {
|
||||
Cursor.FIELD_TYPE_FLOAT -> obj.put(col, it.getDouble(idx))
|
||||
Cursor.FIELD_TYPE_STRING -> obj.put(col, it.getString(idx))
|
||||
Cursor.FIELD_TYPE_NULL -> obj.put(col, JSONObject.NULL)
|
||||
Cursor.FIELD_TYPE_BLOB -> {
|
||||
val blob = it.getBlob(idx)
|
||||
obj.put(col, Base64.encodeToString(blob, Base64.NO_WRAP))
|
||||
}
|
||||
Cursor.FIELD_TYPE_BLOB -> obj.put(col, Base64.encodeToString(it.getBlob(idx), Base64.NO_WRAP))
|
||||
else -> obj.put(col, it.getString(idx))
|
||||
}
|
||||
}
|
||||
}
|
||||
jsonArray.put(obj)
|
||||
arr.put(obj)
|
||||
}
|
||||
}
|
||||
return jsonArray
|
||||
return arr
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
// app/src/main/java/com/dano/test1/LoginManager.kt
|
||||
package com.dano.test1
|
||||
|
||||
import android.content.Context
|
||||
@ -13,18 +14,9 @@ import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import org.json.JSONObject
|
||||
|
||||
object LoginManager {
|
||||
|
||||
private const val SERVER_LOGIN_URL = "http://49.13.157.44/login.php"
|
||||
private val client = OkHttpClient()
|
||||
|
||||
/**
|
||||
* Startet den Login-Prozess.
|
||||
*
|
||||
* @param context Android Context
|
||||
* @param password Vom User eingegebenes Passwort
|
||||
* @param onSuccess Callback mit dem Token wenn Login erfolgreich
|
||||
* @param onError Callback mit Fehlermeldung
|
||||
*/
|
||||
fun loginUser(
|
||||
context: Context,
|
||||
password: String,
|
||||
@ -46,26 +38,18 @@ object LoginManager {
|
||||
|
||||
if (response.isSuccessful && responseText != null) {
|
||||
val json = JSONObject(responseText)
|
||||
if (json.getBoolean("success")) {
|
||||
if (json.optBoolean("success")) {
|
||||
val token = json.getString("token")
|
||||
withContext(Dispatchers.Main) {
|
||||
onSuccess(token)
|
||||
}
|
||||
withContext(Dispatchers.Main) { onSuccess(token) }
|
||||
} else {
|
||||
withContext(Dispatchers.Main) {
|
||||
onError("Login fehlgeschlagen")
|
||||
}
|
||||
withContext(Dispatchers.Main) { onError("Login fehlgeschlagen") }
|
||||
}
|
||||
} else {
|
||||
withContext(Dispatchers.Main) {
|
||||
onError("Fehler beim Login (${response.code})")
|
||||
}
|
||||
withContext(Dispatchers.Main) { onError("Fehler beim Login (${response.code})") }
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("LOGIN", "Exception beim Login", e)
|
||||
withContext(Dispatchers.Main) {
|
||||
onError("Exception: ${e.message}")
|
||||
}
|
||||
Log.e("LOGIN", "Exception", e)
|
||||
withContext(Dispatchers.Main) { onError("Exception: ${e.message}") }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user