Upload via GUI (4909 Dateien)
This commit is contained in:
@@ -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
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<paths>
|
||||||
|
<cache-path name="updates" path="updates/" />
|
||||||
|
</paths>
|
||||||
Reference in New Issue
Block a user