Upload via GUI (8346 Dateien)

This commit is contained in:
2026-10-21 16:54:40 +00:00
parent bb8ffee10d
commit 7fb620e7ca
5 changed files with 348 additions and 201 deletions
+27 -38
View File
@@ -1,47 +1,36 @@
param()
<#
.SYNOPSIS
Baut die signierte Release-App: AAB für den Play Store und APK für direkte Installation.
.DESCRIPTION
Fragt Keystore-Datei und Passwort ab (oder nimmt DESKHUB_RELEASE_* aus der Umgebung). Das Passwort bleibt nur in diesem Prozess.
Ergebnis: artifacts\deskhub-release.aab (Play Store) und artifacts\deskhub-employee-release.apk. Vorher versionCode in build.gradle.kts erhöhen.
#>
param([string]$StoreFile = (Join-Path $env:USERPROFILE 'deskhub-keys\deskhub-release.jks'), [string]$Alias = 'deskhub')
$ErrorActionPreference = 'Stop'
$required = @(
'DESKHUB_RELEASE_STORE_FILE',
'DESKHUB_RELEASE_STORE_PASSWORD',
'DESKHUB_RELEASE_KEY_ALIAS',
'DESKHUB_RELEASE_KEY_PASSWORD'
)
$missing = @($required | Where-Object { [string]::IsNullOrWhiteSpace([Environment]::GetEnvironmentVariable($_)) })
if ($missing.Count -gt 0) {
throw "Fehlende Umgebungsvariablen: $($missing -join ', ')"
if ([string]::IsNullOrWhiteSpace($env:DESKHUB_RELEASE_STORE_FILE)) { $env:DESKHUB_RELEASE_STORE_FILE = $StoreFile }
if ([string]::IsNullOrWhiteSpace($env:DESKHUB_RELEASE_KEY_ALIAS)) { $env:DESKHUB_RELEASE_KEY_ALIAS = $Alias }
if (-not (Test-Path -LiteralPath $env:DESKHUB_RELEASE_STORE_FILE -PathType Leaf)) { throw "Keystore nicht gefunden: $($env:DESKHUB_RELEASE_STORE_FILE). Erst .\new-keystore.ps1 ausführen." }
if ([string]::IsNullOrWhiteSpace($env:DESKHUB_RELEASE_STORE_PASSWORD)) {
$s = Read-Host 'Keystore-Passwort' -AsSecureString
$env:DESKHUB_RELEASE_STORE_PASSWORD = [Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($s))
}
if ([string]::IsNullOrWhiteSpace($env:DESKHUB_RELEASE_KEY_PASSWORD)) { $env:DESKHUB_RELEASE_KEY_PASSWORD = $env:DESKHUB_RELEASE_STORE_PASSWORD }
if (-not (Test-Path (Join-Path $PSScriptRoot 'google-services.json'))) { throw 'google-services.json fehlt im Ordner mobile (aus der Firebase-Konsole).' }
$storeFile = [Environment]::GetEnvironmentVariable('DESKHUB_RELEASE_STORE_FILE')
if (-not (Test-Path -LiteralPath $storeFile -PathType Leaf)) {
throw "Keystore nicht gefunden: $storeFile"
}
$projectRoot = $PSScriptRoot
$javaHome = [Environment]::GetEnvironmentVariable('JAVA_HOME')
if ([string]::IsNullOrWhiteSpace($javaHome)) {
throw 'JAVA_HOME muss auf ein JDK 17 zeigen.'
}
Push-Location $projectRoot
Push-Location $PSScriptRoot
try {
& "$projectRoot\gradlew.bat" clean assembleRelease --no-daemon --console=plain
if ($LASTEXITCODE -ne 0) {
throw "Release-Build fehlgeschlagen (Exitcode $LASTEXITCODE)."
}
$source = "$projectRoot\build\outputs\apk\release\DeskHubMobile-release.apk"
if (-not (Test-Path -LiteralPath $source -PathType Leaf)) {
throw 'Signierte Release-APK wurde nicht erzeugt.'
}
$artifactDirectory = "$projectRoot\artifacts"
New-Item -ItemType Directory -Path $artifactDirectory -Force | Out-Null
$target = "$artifactDirectory\deskhub-employee-release.apk"
Copy-Item -LiteralPath $source -Destination $target -Force
Get-FileHash -Algorithm SHA256 -LiteralPath $target
& "$PSScriptRoot\gradlew.bat" clean bundleRelease assembleRelease --no-daemon --console=plain
if ($LASTEXITCODE -ne 0) { throw "Release-Build fehlgeschlagen (Exitcode $LASTEXITCODE)." }
New-Item -ItemType Directory -Path artifacts -Force | Out-Null
$aab = Get-ChildItem -Recurse -Filter '*.aab' build\outputs\bundle\release | Select-Object -First 1
$apk = "$PSScriptRoot\build\outputs\apk\release\DeskHubMobile-release.apk"
if (-not $aab -or -not (Test-Path $apk)) { throw 'AAB oder APK wurde nicht erzeugt.' }
Copy-Item $aab.FullName artifacts\deskhub-release.aab -Force
Copy-Item $apk artifacts\deskhub-employee-release.apk -Force
Get-FileHash -Algorithm SHA256 artifacts\deskhub-release.aab, artifacts\deskhub-employee-release.apk
} finally {
Pop-Location
Remove-Item Env:\DESKHUB_RELEASE_STORE_PASSWORD, Env:\DESKHUB_RELEASE_KEY_PASSWORD -ErrorAction SilentlyContinue
}
+7 -4
View File
@@ -1,6 +1,7 @@
plugins {
id("com.android.application") version "8.4.2"
id("org.jetbrains.kotlin.android") version "1.9.24"
id("com.google.gms.google-services") version "4.4.4"
}
extra["author"] = "M_Viper"
@@ -19,17 +20,17 @@ val releaseSigningConfigured = listOf(
android {
namespace = "de.deskhub.staff"
compileSdk = 34
compileSdk = 35
defaultConfig {
applicationId = "de.deskhub.staff"
minSdk = 26
targetSdk = 34
targetSdk = 35
// WICHTIG: Bei jedem neuen Debug-Build erhöhen. Android erkennt eine APK mit
// unverändertem versionCode (+ gleicher Signatur) auf manchen Geräten nicht
// zuverlässig als Update, wodurch eine Neuinstallation still fehlschlagen kann.
versionCode = 19
versionName = "2.1.0"
versionCode = 26
versionName = "2.5.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
@@ -101,6 +102,8 @@ dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0")
implementation("com.squareup.okhttp3:okhttp:4.12.0")
implementation("com.google.code.gson:gson:2.11.0")
// Push-Benachrichtigungen (Firebase Cloud Messaging)
implementation("com.google.firebase:firebase-messaging:25.0.1")
testImplementation("junit:junit:4.13.2")
testImplementation("org.robolectric:robolectric:4.13")
+29
View File
@@ -0,0 +1,29 @@
{
"project_info": {
"project_number": "436755419487",
"project_id": "deskhub-96d3f",
"storage_bucket": "deskhub-96d3f.firebasestorage.app"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:436755419487:android:8bad822266b61db37466ea",
"android_client_info": {
"package_name": "de.deskhub.staff"
}
},
"oauth_client": [],
"api_key": [
{
"current_key": "AIzaSyBN6ud1flyG_aFJdv25fsFQpHpxpESiDlk"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": []
}
}
}
],
"configuration_version": "1"
}
+1
View File
@@ -1,3 +1,4 @@
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
android.useAndroidX=true
kotlin.code.style=official
android.suppressUnsupportedCompileSdk=35
@@ -0,0 +1,125 @@
package de.deskhub.staff.push
import android.app.Application
import android.app.NotificationManager
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.Shadows.shadowOf
import org.robolectric.annotation.Config
/** Benachrichtigungen der Push-Nachrichten (auf der JVM mit Robolectric). */
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [34])
class DeskHubPushTest {
private val context: Application get() = ApplicationProvider.getApplicationContext()
private fun grantPermission() = shadowOf(context).grantPermissions(android.Manifest.permission.POST_NOTIFICATIONS)
private fun manager() = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
@Test
fun createsHighImportanceChannel() {
DeskHubPush.ensureChannel(context)
val channel = manager().getNotificationChannel(DeskHubPush.CHANNEL_TICKETS)
assertNotNull(channel)
assertEquals(NotificationManager.IMPORTANCE_HIGH, channel.importance)
}
@Test
fun showsNotificationWithTitleTextAndOpensTicket() {
grantPermission()
val shown = DeskHubPush.show(context, mapOf("type" to "ticket_new", "ticket" to "T-123", "title" to "Neues Ticket", "body" to "#T-123"))
assertTrue(shown)
val posted = shadowOf(manager()).allNotifications
assertEquals(1, posted.size)
val n = posted[0]
assertEquals("Neues Ticket", n.extras.getCharSequence("android.title").toString())
assertEquals("#T-123", n.extras.getCharSequence("android.text").toString())
// Tipp öffnet die App mit der Ticketnummer
val intent = shadowOf(n.contentIntent).savedIntent
assertEquals("T-123", intent.getStringExtra(DeskHubPush.EXTRA_TICKET))
}
@Test
fun sameTicketReplacesEarlierNotification() {
grantPermission()
val data = mapOf("type" to "ticket_reply", "ticket" to "T-7", "title" to "Neue Antwort", "body" to "#T-7")
DeskHubPush.show(context, data)
DeskHubPush.show(context, data)
assertEquals(1, shadowOf(manager()).allNotifications.size)
DeskHubPush.show(context, data + ("ticket" to "T-8"))
assertEquals(2, shadowOf(manager()).allNotifications.size)
}
@Test
fun ignoresUnknownOrEmptyMessages() {
grantPermission()
assertFalse(DeskHubPush.show(context, mapOf("type" to "irgendwas", "title" to "x")))
assertFalse(DeskHubPush.show(context, mapOf("type" to "ticket_new", "title" to " ")))
assertFalse(DeskHubPush.show(context, emptyMap()))
assertEquals(0, shadowOf(manager()).allNotifications.size)
}
@Test
fun showsNothingWithoutPermission() {
shadowOf(context).denyPermissions(android.Manifest.permission.POST_NOTIFICATIONS)
assertFalse(DeskHubPush.canNotify(context))
assertFalse(DeskHubPush.show(context, mapOf("type" to "ticket_new", "ticket" to "T-1", "title" to "Neues Ticket")))
assertEquals(0, shadowOf(manager()).allNotifications.size)
}
@Test
fun customerReplyHasQuickReplyActionOthersDoNot() {
grantPermission()
DeskHubPush.show(context, mapOf("type" to "ticket_reply", "ticket" to "T-5", "title" to "Neue Antwort", "body" to "#T-5"))
DeskHubPush.show(context, mapOf("type" to "ticket_new", "ticket" to "T-6", "title" to "Neues Ticket", "body" to "#T-6"))
val all = shadowOf(manager()).allNotifications
val reply = all.first { it.extras.getCharSequence("android.text").toString() == "#T-5" }
val other = all.first { it.extras.getCharSequence("android.text").toString() == "#T-6" }
assertEquals(1, reply.actions.size)
assertEquals("Antworten", reply.actions[0].title.toString())
assertNotNull(reply.actions[0].remoteInputs)
assertEquals(PushReplyReceiver.KEY_REPLY, reply.actions[0].remoteInputs[0].resultKey)
assertTrue(other.actions == null || other.actions.isEmpty())
}
@Test
fun newEventTypesAreShown() {
grantPermission()
assertTrue(DeskHubPush.show(context, mapOf("type" to "ticket_sla", "ticket" to "T-9", "title" to "SLA überschritten", "body" to "#T-9")))
assertTrue(DeskHubPush.show(context, mapOf("type" to "ticket_mention", "ticket" to "T-9", "title" to "Erwähnung", "body" to "#T-9")))
assertEquals(2, shadowOf(manager()).allNotifications.size)
}
@Test
fun brandNameShowsNextToAppName() {
grantPermission()
DeskHubPush.show(context, mapOf("type" to "ticket_new", "ticket" to "T-20", "title" to "Neues Ticket", "body" to "#T-20", "brand" to "Windel Support"))
DeskHubPush.show(context, mapOf("type" to "ticket_new", "ticket" to "T-21", "title" to "Neues Ticket", "body" to "#T-21"))
val all = shadowOf(manager()).allNotifications
val withBrand = all.first { it.extras.getCharSequence("android.text").toString() == "#T-20" }
val without = all.first { it.extras.getCharSequence("android.text").toString() == "#T-21" }
assertEquals("Windel Support", withBrand.extras.getCharSequence("android.subText").toString())
assertTrue(without.extras.getCharSequence("android.subText") == null)
}
@Test
fun quickReplyTextIsEscapedForHtml() {
assertEquals("a &lt;b&gt; &amp; c<br>d", PushReplyReceiver.toHtml("a <b> & c\nd"))
}
@Test
fun notificationIdsDifferPerTicketAndType() {
assertTrue(DeskHubPush.notificationId("ticket_new", "T-1") != DeskHubPush.notificationId("ticket_new", "T-2"))
assertTrue(DeskHubPush.notificationId("ticket_new", "T-1") != DeskHubPush.notificationId("ticket_reply", "T-1"))
assertEquals(DeskHubPush.notificationId("ticket_new", "T-1"), DeskHubPush.notificationId("ticket_new", "T-1"))
assertTrue(DeskHubPush.notificationId("ticket_new", "T-1") >= 0)
}
}