diff --git a/mobile/build-release.ps1 b/mobile/build-release.ps1
index 769d37ab..9214c6dd 100644
--- a/mobile/build-release.ps1
+++ b/mobile/build-release.ps1
@@ -1,47 +1,36 @@
-param()
-
-$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 ', ')"
-}
-
-$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
-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
-} finally {
- Pop-Location
-}
+<#
+.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'
+
+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).' }
+
+Push-Location $PSScriptRoot
+try {
+ & "$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
+}
diff --git a/mobile/build.gradle.kts b/mobile/build.gradle.kts
index d9c579c0..180b8338 100644
--- a/mobile/build.gradle.kts
+++ b/mobile/build.gradle.kts
@@ -1,154 +1,157 @@
-plugins {
- id("com.android.application") version "8.4.2"
- id("org.jetbrains.kotlin.android") version "1.9.24"
-}
-
-extra["author"] = "M_Viper"
-extra["productName"] = "deskhub"
-
-val releaseStoreFile = providers.environmentVariable("DESKHUB_RELEASE_STORE_FILE").orNull
-val releaseStorePassword = providers.environmentVariable("DESKHUB_RELEASE_STORE_PASSWORD").orNull
-val releaseKeyAlias = providers.environmentVariable("DESKHUB_RELEASE_KEY_ALIAS").orNull
-val releaseKeyPassword = providers.environmentVariable("DESKHUB_RELEASE_KEY_PASSWORD").orNull
-val releaseSigningConfigured = listOf(
- releaseStoreFile,
- releaseStorePassword,
- releaseKeyAlias,
- releaseKeyPassword,
-).all { !it.isNullOrBlank() }
-
-android {
- namespace = "de.deskhub.staff"
- compileSdk = 34
-
- defaultConfig {
- applicationId = "de.deskhub.staff"
- minSdk = 26
- targetSdk = 34
- // 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"
-
- testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
- }
-
- buildFeatures {
- compose = true
- buildConfig = true
- }
-
- composeOptions {
- kotlinCompilerExtensionVersion = "1.5.14"
- }
-
- compileOptions {
- sourceCompatibility = JavaVersion.VERSION_17
- targetCompatibility = JavaVersion.VERSION_17
- }
-
- kotlinOptions {
- jvmTarget = "17"
- }
-
- signingConfigs {
- if (releaseSigningConfigured) {
- create("release") {
- storeFile = file(requireNotNull(releaseStoreFile))
- storePassword = requireNotNull(releaseStorePassword)
- keyAlias = requireNotNull(releaseKeyAlias)
- keyPassword = requireNotNull(releaseKeyPassword)
- }
- }
- }
-
- buildTypes {
- release {
- isMinifyEnabled = true
- if (releaseSigningConfigured) {
- signingConfig = signingConfigs.getByName("release")
- }
- proguardFiles(
- getDefaultProguardFile("proguard-android-optimize.txt"),
- "proguard-rules.pro",
- )
- }
- }
-
- testOptions {
- unitTests.isIncludeAndroidResources = true
- }
-
- packaging {
- resources.excludes += "/META-INF/{AL2.0,LGPL2.1}"
- }
-}
-
-dependencies {
- val composeBom = platform("androidx.compose:compose-bom:2024.09.00")
- implementation(composeBom)
- androidTestImplementation(composeBom)
-
- implementation("androidx.activity:activity-compose:1.9.2")
- implementation("androidx.compose.ui:ui")
- implementation("androidx.compose.ui:ui-tooling-preview")
- implementation("androidx.compose.material3:material3")
- implementation("androidx.compose.material:material-icons-extended")
- implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.3")
- implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.3")
- implementation("androidx.navigation:navigation-compose:2.7.7")
- 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")
-
- testImplementation("junit:junit:4.13.2")
- testImplementation("org.robolectric:robolectric:4.13")
- testImplementation(composeBom)
- testImplementation("androidx.compose.ui:ui-test-junit4")
- testImplementation("androidx.test.ext:junit:1.2.1")
- testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.9.0")
- androidTestImplementation("androidx.test.ext:junit:1.2.1")
- androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1")
- androidTestImplementation("androidx.compose.ui:ui-test-junit4")
- debugImplementation("androidx.compose.ui:ui-tooling")
- debugImplementation("androidx.compose.ui:ui-test-manifest")
-}
-
-val verifyBranding by tasks.registering {
- doLast {
- val strings = layout.projectDirectory
- .file("src/main/res/values/strings.xml")
- .asFile
- .readText()
- check("deskhub" in strings) {
- "app_name muss exakt deskhub lauten."
- }
- val readme = layout.projectDirectory.file("README.md").asFile.readText()
- check("Ersteller: M_Viper" in readme) {
- "README.md muss das Feld Ersteller: M_Viper enthalten."
- }
- check(project.extra["author"] == "M_Viper") {
- "Gradle-Projektmetadatum author muss M_Viper sein."
- }
- }
-}
-
-val exportDebugApk by tasks.registering {
- doLast {
- val source = layout.buildDirectory.file("outputs/apk/debug/DeskHubMobile-debug.apk").get().asFile
- check(source.isFile) { "Debug-APK wurde nicht erzeugt: $source" }
- val target = layout.projectDirectory.file("artifacts/deskhub-employee-debug.apk").asFile
- target.parentFile.mkdirs()
- source.copyTo(target, overwrite = true)
- }
-}
-
-tasks.configureEach {
- if (name == "preBuild") {
- dependsOn(verifyBranding)
- }
- if (name == "assembleDebug") {
- finalizedBy(exportDebugApk)
- }
-}
+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"
+extra["productName"] = "deskhub"
+
+val releaseStoreFile = providers.environmentVariable("DESKHUB_RELEASE_STORE_FILE").orNull
+val releaseStorePassword = providers.environmentVariable("DESKHUB_RELEASE_STORE_PASSWORD").orNull
+val releaseKeyAlias = providers.environmentVariable("DESKHUB_RELEASE_KEY_ALIAS").orNull
+val releaseKeyPassword = providers.environmentVariable("DESKHUB_RELEASE_KEY_PASSWORD").orNull
+val releaseSigningConfigured = listOf(
+ releaseStoreFile,
+ releaseStorePassword,
+ releaseKeyAlias,
+ releaseKeyPassword,
+).all { !it.isNullOrBlank() }
+
+android {
+ namespace = "de.deskhub.staff"
+ compileSdk = 35
+
+ defaultConfig {
+ applicationId = "de.deskhub.staff"
+ minSdk = 26
+ 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 = 26
+ versionName = "2.5.0"
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ buildFeatures {
+ compose = true
+ buildConfig = true
+ }
+
+ composeOptions {
+ kotlinCompilerExtensionVersion = "1.5.14"
+ }
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+
+ kotlinOptions {
+ jvmTarget = "17"
+ }
+
+ signingConfigs {
+ if (releaseSigningConfigured) {
+ create("release") {
+ storeFile = file(requireNotNull(releaseStoreFile))
+ storePassword = requireNotNull(releaseStorePassword)
+ keyAlias = requireNotNull(releaseKeyAlias)
+ keyPassword = requireNotNull(releaseKeyPassword)
+ }
+ }
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = true
+ if (releaseSigningConfigured) {
+ signingConfig = signingConfigs.getByName("release")
+ }
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro",
+ )
+ }
+ }
+
+ testOptions {
+ unitTests.isIncludeAndroidResources = true
+ }
+
+ packaging {
+ resources.excludes += "/META-INF/{AL2.0,LGPL2.1}"
+ }
+}
+
+dependencies {
+ val composeBom = platform("androidx.compose:compose-bom:2024.09.00")
+ implementation(composeBom)
+ androidTestImplementation(composeBom)
+
+ implementation("androidx.activity:activity-compose:1.9.2")
+ implementation("androidx.compose.ui:ui")
+ implementation("androidx.compose.ui:ui-tooling-preview")
+ implementation("androidx.compose.material3:material3")
+ implementation("androidx.compose.material:material-icons-extended")
+ implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.3")
+ implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.3")
+ implementation("androidx.navigation:navigation-compose:2.7.7")
+ 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")
+ testImplementation(composeBom)
+ testImplementation("androidx.compose.ui:ui-test-junit4")
+ testImplementation("androidx.test.ext:junit:1.2.1")
+ testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.9.0")
+ androidTestImplementation("androidx.test.ext:junit:1.2.1")
+ androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1")
+ androidTestImplementation("androidx.compose.ui:ui-test-junit4")
+ debugImplementation("androidx.compose.ui:ui-tooling")
+ debugImplementation("androidx.compose.ui:ui-test-manifest")
+}
+
+val verifyBranding by tasks.registering {
+ doLast {
+ val strings = layout.projectDirectory
+ .file("src/main/res/values/strings.xml")
+ .asFile
+ .readText()
+ check("deskhub" in strings) {
+ "app_name muss exakt deskhub lauten."
+ }
+ val readme = layout.projectDirectory.file("README.md").asFile.readText()
+ check("Ersteller: M_Viper" in readme) {
+ "README.md muss das Feld Ersteller: M_Viper enthalten."
+ }
+ check(project.extra["author"] == "M_Viper") {
+ "Gradle-Projektmetadatum author muss M_Viper sein."
+ }
+ }
+}
+
+val exportDebugApk by tasks.registering {
+ doLast {
+ val source = layout.buildDirectory.file("outputs/apk/debug/DeskHubMobile-debug.apk").get().asFile
+ check(source.isFile) { "Debug-APK wurde nicht erzeugt: $source" }
+ val target = layout.projectDirectory.file("artifacts/deskhub-employee-debug.apk").asFile
+ target.parentFile.mkdirs()
+ source.copyTo(target, overwrite = true)
+ }
+}
+
+tasks.configureEach {
+ if (name == "preBuild") {
+ dependsOn(verifyBranding)
+ }
+ if (name == "assembleDebug") {
+ finalizedBy(exportDebugApk)
+ }
+}
diff --git a/mobile/google-services.json b/mobile/google-services.json
new file mode 100644
index 00000000..d1edcdf6
--- /dev/null
+++ b/mobile/google-services.json
@@ -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"
+}
\ No newline at end of file
diff --git a/mobile/gradle.properties b/mobile/gradle.properties
index c70be5a5..ea64512f 100644
--- a/mobile/gradle.properties
+++ b/mobile/gradle.properties
@@ -1,3 +1,4 @@
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
android.useAndroidX=true
kotlin.code.style=official
+android.suppressUnsupportedCompileSdk=35
diff --git a/mobile/src/test/java/de/deskhub/staff/push/DeskHubPushTest.kt b/mobile/src/test/java/de/deskhub/staff/push/DeskHubPushTest.kt
new file mode 100644
index 00000000..a004b2bc
--- /dev/null
+++ b/mobile/src/test/java/de/deskhub/staff/push/DeskHubPushTest.kt
@@ -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 <b> & c
d", PushReplyReceiver.toHtml("a & 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)
+ }
+}