Upload via GUI (4909 Dateien)

This commit is contained in:
2026-10-25 17:57:13 +00:00
parent d73e0d5e6a
commit e3f32f553a
5 changed files with 1951 additions and 1811 deletions
@@ -5,6 +5,8 @@ import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Handler
import android.os.Looper
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
@@ -22,6 +24,14 @@ import de.deskhub.staff.ui.DeskHubViewModel
import de.deskhub.staff.ui.DeskHubViewModelFactory import de.deskhub.staff.ui.DeskHubViewModelFactory
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
private val connectionPingHandler = Handler(Looper.getMainLooper())
private val connectionPing = object : Runnable {
override fun run() {
AppConnectionPing.send(applicationContext)
connectionPingHandler.postDelayed(this, 4 * 60 * 1000L)
}
}
private val viewModel: DeskHubViewModel by viewModels { private val viewModel: DeskHubViewModel by viewModels {
DeskHubViewModelFactory(TicketRepository(SecureSessionStore(applicationContext))) DeskHubViewModelFactory(TicketRepository(SecureSessionStore(applicationContext)))
} }
@@ -35,9 +45,16 @@ class MainActivity : ComponentActivity() {
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
connectionPingHandler.removeCallbacks(connectionPing)
connectionPing.run()
DeskHubWidget.requestUpdate(applicationContext) DeskHubWidget.requestUpdate(applicationContext)
} }
override fun onPause() {
connectionPingHandler.removeCallbacks(connectionPing)
super.onPause()
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
de.deskhub.staff.ui.I18n.init(applicationContext) de.deskhub.staff.ui.I18n.init(applicationContext)
@@ -48,9 +65,12 @@ class MainActivity : ComponentActivity() {
// Nach der Anmeldung: um Erlaubnis für Benachrichtigungen bitten (einmal) und das Gerät für Push anmelden // Nach der Anmeldung: um Erlaubnis für Benachrichtigungen bitten (einmal) und das Gerät für Push anmelden
LaunchedEffect(signedIn) { LaunchedEffect(signedIn) {
if (signedIn) { if (signedIn) {
AppConnectionPing.scheduleBackground(applicationContext)
requestNotificationPermissionOnce() requestNotificationPermissionOnce()
PushRegistration.registerCurrent(applicationContext) PushRegistration.registerCurrent(applicationContext)
CrashReporter.sendPending(applicationContext) CrashReporter.sendPending(applicationContext)
} else {
AppConnectionPing.cancelBackground(applicationContext)
} }
} }
DeskHubApp(viewModel) DeskHubApp(viewModel)
@@ -86,4 +106,4 @@ class MainActivity : ComponentActivity() {
prefs.edit().putBoolean("asked_notifications", true).apply() prefs.edit().putBoolean("asked_notifications", true).apply()
notificationPermission.launch(Manifest.permission.POST_NOTIFICATIONS) notificationPermission.launch(Manifest.permission.POST_NOTIFICATIONS)
} }
} }
@@ -168,7 +168,7 @@ class TicketRepository(
email = email.trim(), email = email.trim(),
password = password, password = password,
totpCode = totpCode?.takeIf { it.isNotBlank() }, totpCode = totpCode?.takeIf { it.isNotBlank() },
deviceName = "${Build.MANUFACTURER} ${Build.MODEL}".trim(), deviceName = "Android (${Build.MANUFACTURER} ${Build.MODEL})".trim(),
), ),
) )
if (response.requiresTotp) return LoginResult.TotpRequired if (response.requiresTotp) return LoginResult.TotpRequired
@@ -319,6 +319,11 @@ class TicketRepository(
return requireApi().createTicket(request).ticket.toDomain() return requireApi().createTicket(request).ticket.toDomain()
} }
suspend fun appUpdate() = requireApi().appUpdate()
suspend fun downloadAppUpdate(target: java.io.File, sha256: String, onProgress: (Int) -> Unit) =
requireApi().downloadAppUpdate(target, sha256, onProgress)
suspend fun activitySince(since: String): ActivityUpdate { suspend fun activitySince(since: String): ActivityUpdate {
val response = requireApi().activity(since) val response = requireApi().activity(since)
return ActivityUpdate(response.newReplies, response.newUnassigned, response.serverTime, response.health.map { HealthIssue(it.level, it.message) }) return ActivityUpdate(response.newReplies, response.newUnassigned, response.serverTime, response.health.map { HealthIssue(it.level, it.message) })
@@ -267,6 +267,7 @@ private fun StaffNavigation(state: DeskHubState, viewModel: DeskHubViewModel) {
) { outerPadding -> ) { outerPadding ->
Column(Modifier.padding(outerPadding)) { Column(Modifier.padding(outerPadding)) {
StatusBanners(state, onRetry = viewModel::retryConnection) StatusBanners(state, onRetry = viewModel::retryConnection)
UpdateBanner(state, viewModel)
NavHost( NavHost(
navController = navController, navController = navController,
startDestination = "dashboard", startDestination = "dashboard",
@@ -652,6 +653,69 @@ internal fun StatusBanners(state: DeskHubState, onRetry: () -> Unit) {
} }
} }
/** Hinweis „Update verfügbar“ mit Download und anschließender Installation (Android fragt dafür einmal nach der Erlaubnis). */
@Composable
internal fun UpdateBanner(state: DeskHubState, viewModel: DeskHubViewModel) {
val context = LocalContext.current
val current = remember(context) { runCatching { context.packageManager.getPackageInfo(context.packageName, 0).versionName }.getOrNull().orEmpty() }
// Aus dem Play Store installierte Apps aktualisiert der Store selbst – Selbst-Update nur für direkt installierte APKs
val fromPlayStore = remember(context) {
runCatching {
if (android.os.Build.VERSION.SDK_INT >= 30) context.packageManager.getInstallSourceInfo(context.packageName).installingPackageName
else @Suppress("DEPRECATION") context.packageManager.getInstallerPackageName(context.packageName)
}.getOrNull() == "com.android.vending"
}
LaunchedEffect(state.staff?.id) {
if (state.staff != null && current.isNotBlank() && !fromPlayStore) {
while (true) {
viewModel.checkAppUpdate(current)
kotlinx.coroutines.delay(6 * 3600 * 1000L)
}
}
}
LaunchedEffect(state.appUpdateFile) {
val file = state.appUpdateFile ?: return@LaunchedEffect
viewModel.appUpdateFileHandled()
if (android.os.Build.VERSION.SDK_INT >= 26 && !context.packageManager.canRequestPackageInstalls()) {
// Einmalige Erlaubnis „Aus dieser Quelle installieren“ – danach erneut auf „Jetzt aktualisieren“ tippen
context.startActivity(
android.content.Intent(android.provider.Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES, android.net.Uri.parse("package:${context.packageName}"))
.addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK),
)
return@LaunchedEffect
}
val uri = androidx.core.content.FileProvider.getUriForFile(context, "${context.packageName}.updates", file)
context.startActivity(
android.content.Intent(android.content.Intent.ACTION_VIEW)
.setDataAndType(uri, "application/vnd.android.package-archive")
.addFlags(android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION or android.content.Intent.FLAG_ACTIVITY_NEW_TASK),
)
}
val info = state.appUpdate ?: return
val firstBanner = !state.offline && state.healthIssues.isEmpty() // nur das oberste Banner braucht Abstand zur Statusleiste
Surface(color = MaterialTheme.colorScheme.primaryContainer, modifier = Modifier.fillMaxWidth()) {
Column(Modifier.then(if (firstBanner) Modifier.statusBarsPadding() else Modifier).padding(start = 16.dp, end = 8.dp, top = 10.dp, bottom = 4.dp)) {
Text(
state.appUpdateProgress?.let { "Update ${info.version} wird geladen … $it %" }
?: ("Update verfügbar: DeskHub ${info.version} (installiert: $current)" + if (info.notes.isNotBlank()) "\n${info.notes}" else ""),
modifier = Modifier.padding(end = 8.dp),
color = MaterialTheme.colorScheme.onPrimaryContainer,
style = MaterialTheme.typography.labelLarge,
)
state.appUpdateProgress?.let {
androidx.compose.material3.LinearProgressIndicator(progress = { it / 100f }, modifier = Modifier.fillMaxWidth().padding(top = 8.dp, bottom = 6.dp, end = 8.dp))
}
state.appUpdateError?.let { Text(it, color = MaterialTheme.colorScheme.error, style = MaterialTheme.typography.bodySmall, modifier = Modifier.padding(top = 4.dp)) }
if (state.appUpdateProgress == null) {
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) {
TextButton(onClick = viewModel::dismissAppUpdate) { Text("Später") }
TextButton(onClick = { viewModel.downloadAppUpdate(context.cacheDir) }) { Text("Jetzt aktualisieren") }
}
}
}
}
}
private fun androidx.navigation.NavController.navigateToTab(route: String) { private fun androidx.navigation.NavController.navigateToTab(route: String) {
navigate(route) { navigate(route) {
popUpTo(graph.findStartDestination().id) { saveState = true } popUpTo(graph.findStartDestination().id) { saveState = true }
File diff suppressed because it is too large Load Diff
+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<paths>
<cache-path name="updates" path="updates/" />
</paths>