Upload via GUI (8346 Dateien)
This commit is contained in:
+27
-38
@@ -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
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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,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 <b> & 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user