Upload via GUI (4909 Dateien)
This commit is contained in:
Binary file not shown.
@@ -3,6 +3,8 @@
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<!-- Popups bei neuen Tickets (ab Android 13 fragt die App einmal nach) -->
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<!-- Für „Jetzt aktualisieren“: die heruntergeladene neue Version installieren -->
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||
|
||||
<application
|
||||
android:name=".DeskHubApplication"
|
||||
@@ -23,6 +25,15 @@
|
||||
<meta-data
|
||||
android:name="com.google.firebase.messaging.default_notification_channel_id"
|
||||
android:value="tickets" />
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.updates"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/update_paths" />
|
||||
</provider>
|
||||
<service
|
||||
android:name=".push.DeskHubMessagingService"
|
||||
android:exported="false">
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,66 @@
|
||||
package de.deskhub.staff
|
||||
|
||||
import android.content.Context
|
||||
import androidx.work.Constraints
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.ExistingPeriodicWorkPolicy
|
||||
import androidx.work.NetworkType
|
||||
import androidx.work.PeriodicWorkRequestBuilder
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.WorkerParameters
|
||||
import de.deskhub.staff.data.remote.DeskHubApi
|
||||
import de.deskhub.staff.data.security.SecureSessionStore
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
/** Sends a liveness request without changing the staff member's presence status. */
|
||||
class AppConnectionWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {
|
||||
override suspend fun doWork(): Result {
|
||||
val session = SecureSessionStore(applicationContext).load() ?: return Result.success()
|
||||
if (session.token.isBlank() || session.serverUrl.isBlank()) return Result.success()
|
||||
return try {
|
||||
DeskHubApi(session.serverUrl) { session.token }.me()
|
||||
Result.success()
|
||||
} catch (cancelled: CancellationException) {
|
||||
throw cancelled
|
||||
} catch (_: Exception) {
|
||||
Result.retry()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object AppConnectionPing {
|
||||
private const val WORK_NAME = "deskhub-app-connection-heartbeat"
|
||||
|
||||
/** Quick heartbeat while the app is visible. */
|
||||
fun send(context: Context) {
|
||||
val app = context.applicationContext
|
||||
thread(name = "app-connection-ping") {
|
||||
try {
|
||||
val session = SecureSessionStore(app).load() ?: return@thread
|
||||
if (session.token.isBlank() || session.serverUrl.isBlank()) return@thread
|
||||
runBlocking { DeskHubApi(session.serverUrl) { session.token }.me() }
|
||||
} catch (_: Exception) {
|
||||
// The next foreground or background heartbeat will retry.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Android schedules this network-constrained worker periodically, including in the background. */
|
||||
fun scheduleBackground(context: Context) {
|
||||
val constraints = Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
.build()
|
||||
val request = PeriodicWorkRequestBuilder<AppConnectionWorker>(15, TimeUnit.MINUTES)
|
||||
.setConstraints(constraints)
|
||||
.build()
|
||||
WorkManager.getInstance(context.applicationContext)
|
||||
.enqueueUniquePeriodicWork(WORK_NAME, ExistingPeriodicWorkPolicy.UPDATE, request)
|
||||
}
|
||||
|
||||
fun cancelBackground(context: Context) {
|
||||
WorkManager.getInstance(context.applicationContext).cancelUniqueWork(WORK_NAME)
|
||||
}
|
||||
}
|
||||
@@ -227,6 +227,47 @@ class DeskHubApi(
|
||||
suspend fun activity(since: String): ActivityResponseDto =
|
||||
execute("activity.php", "GET", null, ActivityResponseDto::class.java, query = mapOf("since" to since))
|
||||
|
||||
/** Neueste veröffentlichte Android-Version (Quelle: Plugin-Webseite, über den DeskHub-Server). */
|
||||
suspend fun appUpdate(): de.deskhub.staff.data.remote.dto.AppUpdateDto =
|
||||
execute("app_update.php", "GET", null, de.deskhub.staff.data.remote.dto.AppUpdateDto::class.java, query = mapOf("platform" to "android"))
|
||||
|
||||
/** Lädt die APK nach [target] und prüft die SHA-256-Prüfsumme; [onProgress] liefert 0–100. */
|
||||
suspend fun downloadAppUpdate(target: java.io.File, expectedSha256: String, onProgress: (Int) -> Unit) = withContext(Dispatchers.IO) {
|
||||
val token = tokenProvider() ?: throw ApiException(401, "UNAUTHENTICATED", "Keine lokale Anmeldung vorhanden.")
|
||||
val request = Request.Builder()
|
||||
.url("$baseUrl/api/staff/v1/app_update.php".toHttpUrl().newBuilder().addQueryParameter("platform", "android").addQueryParameter("download", "1").build())
|
||||
.header("Authorization", "Bearer $token")
|
||||
.header("X-DeskHub-Authorization", "Bearer $token")
|
||||
.build()
|
||||
val longClient = client.newBuilder().readTimeout(120, TimeUnit.SECONDS).build()
|
||||
val response = try { longClient.newCall(request).execute() } catch (e: java.io.IOException) { throw ApiException(0, "NETWORK", "Keine Verbindung zum Server.") }
|
||||
response.use { r ->
|
||||
if (!r.isSuccessful) throw ApiException(r.code, "HTTP_${r.code}", "Download fehlgeschlagen (${r.code}).")
|
||||
val body = r.body ?: throw ApiException(r.code, "EMPTY_RESPONSE", "Der Server hat keine Datei geliefert.")
|
||||
val total = body.contentLength()
|
||||
val digest = java.security.MessageDigest.getInstance("SHA-256")
|
||||
target.parentFile?.mkdirs()
|
||||
var got = 0L
|
||||
var last = -1
|
||||
body.byteStream().use { input ->
|
||||
target.outputStream().use { output ->
|
||||
val buffer = ByteArray(64 * 1024)
|
||||
while (true) {
|
||||
val n = input.read(buffer)
|
||||
if (n < 0) break
|
||||
output.write(buffer, 0, n); digest.update(buffer, 0, n); got += n
|
||||
if (total > 0) { val pct = (got * 100 / total).toInt(); if (pct != last) { last = pct; onProgress(pct) } }
|
||||
}
|
||||
}
|
||||
}
|
||||
val sha = digest.digest().joinToString("") { "%02x".format(it) }
|
||||
if (!sha.equals(expectedSha256, ignoreCase = true)) {
|
||||
target.delete()
|
||||
throw ApiException(0, "CHECKSUM", "Die heruntergeladene Datei ist beschädigt (Prüfsumme stimmt nicht). Bitte erneut versuchen.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun logout() {
|
||||
execute("logout.php", "POST", emptyMap<String, String>(), Map::class.java)
|
||||
}
|
||||
|
||||
@@ -1211,3 +1211,11 @@ data class AutoCloseSaveRequest(val action: String = "save_auto_close", val enab
|
||||
// ─── Bild-Upload (z.B. für Signaturen) ───────────────────────────────────────
|
||||
|
||||
data class UploadImageResponse(val url: String = "")
|
||||
|
||||
data class AppUpdateDto(
|
||||
val available: Boolean = false,
|
||||
val version: String = "",
|
||||
val sha256: String = "",
|
||||
val size: Long = 0,
|
||||
val notes: String = "",
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user