Skip to content

Commit

Permalink
Feature: push notification version support (#12)
Browse files Browse the repository at this point in the history
* Discard invalid notifications.

* Run junit tests on github workflows.
  • Loading branch information
TimOrtel authored Nov 15, 2023
1 parent 477b806 commit 07bff0c
Show file tree
Hide file tree
Showing 14 changed files with 354 additions and 63 deletions.
36 changes: 36 additions & 0 deletions .github/workflows/unit-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: JUnit Tests (no end-to-end)

on:
push:
branches:
- main
pull_request:
workflow_dispatch:

jobs:
jUnit:
runs-on: ubuntu-latest
timeout-minutes: 60

steps:
- uses: actions/checkout@v4

- name: Gradle Wrapper Verification
uses: gradle/wrapper-validation-action@v1

- name: JDK setup
uses: actions/setup-java@v3
with:
distribution: 'zulu'
java-version: 17

- name: run tests
run: ./gradlew test -Dskip.unit-tests=false -Dskip.e2e=true -Dskip.debugVariants=true -Dskip.flavor.unrestricted=true -Dskip.flavor.beta=true

- name: Test Report
uses: dorny/test-reporter@v1
if: success() || failure() # run this step even if previous step failed
with:
name: Android Unit Tests
path: test-outputs/**/*.xml
reporter: java-junit
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,12 @@ class AndroidLibraryConventionPlugin : Plugin<Project> {
}
}

if (Boolean.getBoolean("skip.unit-tests")) {
useJUnit {
excludeCategories("de.tum.informatics.www1.artemis.native_app.core.common.test.UnitTest")
}
}

reports.junitXml.required.set(true)
reports.junitXml.outputLocation.set(rootProject.rootDir.resolve("test-outputs/${project.name}/$name/"))
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package de.tum.informatics.www1.artemis.native_app.core.common.test

interface UnitTest
1 change: 1 addition & 0 deletions docker/e2e-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ services:
- -Dskip.debugVariants=true
- -Dskip.flavor.unrestricted=true
- -Dskip.flavor.beta=true
- -Dskip.unit-tests=true
networks:
- artemis
volumes:
Expand Down
3 changes: 3 additions & 0 deletions feature/push/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,7 @@ dependencies {

testImplementation(project(":feature:login"))
testImplementation(project(":feature:login-test"))

testImplementation(libs.mockk.android)
testImplementation(libs.mockk.agent)
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ import de.tum.informatics.www1.artemis.native_app.feature.push.notification_mode
import de.tum.informatics.www1.artemis.native_app.feature.push.notification_model.NotificationType
import de.tum.informatics.www1.artemis.native_app.feature.push.notification_model.communicationType
import de.tum.informatics.www1.artemis.native_app.feature.push.service.NotificationManager
import de.tum.informatics.www1.artemis.native_app.feature.push.service.PushNotificationCipher
import de.tum.informatics.www1.artemis.native_app.feature.push.service.PushNotificationConfigurationService
import de.tum.informatics.www1.artemis.native_app.feature.push.service.PushNotificationHandler
import de.tum.informatics.www1.artemis.native_app.feature.push.service.PushNotificationJobService
import de.tum.informatics.www1.artemis.native_app.feature.push.service.impl.notification_manager.NotificationTargetManager
import kotlinx.coroutines.flow.first
Expand Down Expand Up @@ -43,10 +45,11 @@ class ArtemisFirebaseMessagingService : FirebaseMessagingService() {
}
}

private val pushNotificationCipher: PushNotificationCipher = get()
private val pushNotificationHandler: PushNotificationHandler = get()
private val pushNotificationJobService: PushNotificationJobService = get()
private val pushNotificationConfigurationService: PushNotificationConfigurationService = get()
private val serverConfigurationService: ServerConfigurationService = get()
private val notificationManager: NotificationManager = get()

/**
* Whenever this functions is called we need to synchronize the new token with the server.
Expand All @@ -72,66 +75,19 @@ class ArtemisFirebaseMessagingService : FirebaseMessagingService() {
}

// In this method we only have 10 seconds to handle the notification
// Furthermore, it is not possible to use dispatching coroutines here, therefore we rely on runBlocking
override fun onMessageReceived(message: RemoteMessage) {
Log.d(TAG, "New push notification received")

val payloadCiphertext = message.data["payload"] ?: return
val iv = message.data["iv"] ?: return

val payload: String = runBlocking {
val key =
pushNotificationConfigurationService.getCurrentAESKey() ?: return@runBlocking null
val cipher = cipher ?: return@runBlocking null

val ivAsBytes = Base64.decode(iv.toByteArray(Charsets.ISO_8859_1), Base64.DEFAULT)

cipher.decrypt(payloadCiphertext, key, ivAsBytes) ?: return@runBlocking null
} ?: return

val notification: ArtemisNotification<NotificationType> = Json.decodeFromString(payload)

// if the metis context this notification is about is already visible we do not pop that notification.
val currentActivity = (application as? CurrentActivityListener)?.currentActivity?.value

val visibleMetisContexts: List<VisibleMetisContext> =
(currentActivity as? VisibleMetisContextReporter)?.visibleMetisContexts?.value.orEmpty()

val notificationType = notification.type
if (notificationType is CommunicationNotificationType) {
val metisTarget = NotificationTargetManager.getCommunicationNotificationTarget(
notificationType.communicationType,
notification.target
)

val visibleMetisContext =
VisibleStandalonePostDetails(
metisTarget.metisContext,
metisTarget.postId
)

// If the context is already visible cancel now!
if (visibleMetisContext in visibleMetisContexts) return
val payload: String? =
pushNotificationCipher.decipherPushNotification(payloadCiphertext, iv)
if (payload == null) {
Log.d(TAG, "Could not decipher push notification")
return
}

runBlocking {
notificationManager.popNotification(
context = this@ArtemisFirebaseMessagingService,
artemisNotification = notification
)
}
}

private fun Cipher.decrypt(ciphertext: String, key: SecretKey, iv: ByteArray): String? {
return try {
init(Cipher.DECRYPT_MODE, key, IvParameterSpec(iv))

val cipherTextBytes = ciphertext.toByteArray(Charsets.ISO_8859_1)
val textBytes = doFinal(Base64.decode(cipherTextBytes, Base64.DEFAULT))

String(textBytes, Charsets.UTF_8)
} catch (e: Exception) {
null
}
pushNotificationHandler.handleServerPushNotification(payload)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,16 @@ sealed interface ArtemisNotification<T : NotificationType> {
val notificationPlaceholders: List<String>
val target: String
val date: Instant
val version: Int
}

@Serializable
data class MiscArtemisNotification(
override val type: MiscNotificationType,
override val notificationPlaceholders: List<String>,
override val target: String,
override val date: Instant
override val date: Instant,
override val version: Int
) : ArtemisNotification<MiscNotificationType>

@Serializable
Expand All @@ -38,22 +40,24 @@ data class CommunicationArtemisNotification(
override val type: CommunicationNotificationType,
override val notificationPlaceholders: List<String>,
override val target: String,
override val date: Instant
override val date: Instant,
override val version: Int
) : ArtemisNotification<CommunicationNotificationType>

@Serializable
data class UnknownArtemisNotification(
override val type: UnknownNotificationType,
override val notificationPlaceholders: List<String>,
override val target: String,
override val date: Instant
override val date: Instant,
override val version: Int
) : ArtemisNotification<UnknownNotificationType>

private val nameToTypeMapping: Map<String, NotificationType> = (
StandalonePostCommunicationNotificationType.values().toList() +
ReplyPostCommunicationNotificationType.values().toList() +
MiscNotificationType.values().toList() +
ConversationNotificationType.values().toList()
StandalonePostCommunicationNotificationType.entries +
ReplyPostCommunicationNotificationType.entries +
MiscNotificationType.entries +
ConversationNotificationType.entries
).associateBy { it.name }

object ArtemisNotificationDeserializer :
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@ package de.tum.informatics.www1.artemis.native_app.feature.push

import de.tum.informatics.www1.artemis.native_app.feature.push.service.CommunicationNotificationManager
import de.tum.informatics.www1.artemis.native_app.feature.push.service.NotificationManager
import de.tum.informatics.www1.artemis.native_app.feature.push.service.PushNotificationCipher
import de.tum.informatics.www1.artemis.native_app.feature.push.service.PushNotificationConfigurationService
import de.tum.informatics.www1.artemis.native_app.feature.push.service.PushNotificationHandler
import de.tum.informatics.www1.artemis.native_app.feature.push.service.PushNotificationJobService
import de.tum.informatics.www1.artemis.native_app.feature.push.service.impl.PushNotificationCipherImpl
import de.tum.informatics.www1.artemis.native_app.feature.push.service.impl.PushNotificationConfigurationServiceImpl
import de.tum.informatics.www1.artemis.native_app.feature.push.service.impl.PushNotificationHandlerImpl
import de.tum.informatics.www1.artemis.native_app.feature.push.service.impl.UnsubscribeFromNotificationsWorker
import de.tum.informatics.www1.artemis.native_app.feature.push.service.impl.UploadPushNotificationDeviceConfigurationWorker
import de.tum.informatics.www1.artemis.native_app.feature.push.service.impl.WorkManagerPushNotificationJobService
Expand All @@ -15,6 +19,7 @@ import de.tum.informatics.www1.artemis.native_app.feature.push.service.impl.noti
import de.tum.informatics.www1.artemis.native_app.feature.push.service.network.NotificationSettingsService
import de.tum.informatics.www1.artemis.native_app.feature.push.service.network.impl.NotificationSettingsServiceImpl
import de.tum.informatics.www1.artemis.native_app.feature.push.ui.PushNotificationSettingsViewModel
import org.koin.android.ext.koin.androidApplication
import org.koin.android.ext.koin.androidContext
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.androidx.workmanager.dsl.workerOf
Expand All @@ -33,7 +38,14 @@ val pushModule = module {

single<NotificationManager> { NotificationManagerImpl(get(), get()) }
single { MiscNotificationManager(androidContext()) }
single<CommunicationNotificationManager> { CommunicationNotificationManagerImpl(androidContext(), get()) }
single<CommunicationNotificationManager> {
CommunicationNotificationManagerImpl(
androidContext(),
get()
)
}
single<PushNotificationCipher> { PushNotificationCipherImpl(get()) }
single<PushNotificationHandler> { PushNotificationHandlerImpl(androidApplication(), get()) }

workerOf(::UploadPushNotificationDeviceConfigurationWorker)
workerOf(::UnsubscribeFromNotificationsWorker)
Expand All @@ -45,4 +57,4 @@ val pushModule = module {
)
}
viewModel { PushNotificationSettingsViewModel(get(), get(), get(), get(), get()) }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package de.tum.informatics.www1.artemis.native_app.feature.push.service

interface PushNotificationCipher {

fun decipherPushNotification(ciphertext: String, iv: String): String?
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package de.tum.informatics.www1.artemis.native_app.feature.push.service

interface PushNotificationHandler {

/**
* Handles the receiving of a push notification. The push notification has to be decrypted.
* Based on the type stores it and shows it to the user.
*/
fun handleServerPushNotification(payload: String)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package de.tum.informatics.www1.artemis.native_app.feature.push.service.impl

import android.util.Base64
import de.tum.informatics.www1.artemis.native_app.feature.push.service.PushNotificationCipher
import de.tum.informatics.www1.artemis.native_app.feature.push.service.PushNotificationConfigurationService
import kotlinx.coroutines.runBlocking
import java.security.NoSuchAlgorithmException
import javax.crypto.Cipher
import javax.crypto.NoSuchPaddingException
import javax.crypto.SecretKey
import javax.crypto.spec.IvParameterSpec

class PushNotificationCipherImpl(
private val pushNotificationConfigurationService: PushNotificationConfigurationService
) : PushNotificationCipher {

private companion object {
private const val ALGORITHM = "AES/CBC/PKCS7Padding"

private val cipher: Cipher? = try {
Cipher.getInstance(ALGORITHM)
} catch (e: NoSuchAlgorithmException) {
null
} catch (e: NoSuchPaddingException) {
null
}
}

override fun decipherPushNotification(ciphertext: String, iv: String): String? = runBlocking {
val key = pushNotificationConfigurationService.getCurrentAESKey() ?: return@runBlocking null
val cipher = cipher ?: return@runBlocking null

val ivAsBytes = Base64.decode(iv.toByteArray(Charsets.ISO_8859_1), Base64.DEFAULT)

cipher.decrypt(ciphertext, key, ivAsBytes) ?: return@runBlocking null
}

private fun Cipher.decrypt(ciphertext: String, key: SecretKey, iv: ByteArray): String? {
return try {
init(Cipher.DECRYPT_MODE, key, IvParameterSpec(iv))

val cipherTextBytes = ciphertext.toByteArray(Charsets.ISO_8859_1)
val textBytes = doFinal(Base64.decode(cipherTextBytes, Base64.DEFAULT))

String(textBytes, Charsets.UTF_8)
} catch (e: Exception) {
null
}
}
}
Loading

0 comments on commit 07bff0c

Please sign in to comment.