Merge pull request #1899 from vector-im/feature/bma/testNotificationFactory
Test notification factory
This commit is contained in:
commit
b8436fc77e
23 changed files with 1002 additions and 148 deletions
|
|
@ -236,11 +236,11 @@ koverMerged {
|
|||
name = "Global minimum code coverage."
|
||||
target = kotlinx.kover.api.VerificationTarget.ALL
|
||||
bound {
|
||||
minValue = 60
|
||||
minValue = 65
|
||||
// Setting a max value, so that if coverage is bigger, it means that we have to change minValue.
|
||||
// For instance if we have minValue = 20 and maxValue = 30, and current code coverage is now 31.32%, update
|
||||
// minValue to 25 and maxValue to 35.
|
||||
maxValue = 70
|
||||
maxValue = 75
|
||||
counter = kotlinx.kover.api.CounterType.INSTRUCTION
|
||||
valueType = kotlinx.kover.api.VerificationValueType.COVERED_PERCENTAGE
|
||||
}
|
||||
|
|
|
|||
|
|
@ -136,6 +136,7 @@ test_appyx_junit = { module = "com.bumble.appyx:testing-junit4", version.ref = "
|
|||
coil = { module = "io.coil-kt:coil", version.ref = "coil" }
|
||||
coil_compose = { module = "io.coil-kt:coil-compose", version.ref = "coil" }
|
||||
coil_gif = { module = "io.coil-kt:coil-gif", version.ref = "coil" }
|
||||
coil_test = { module = "io.coil-kt:coil-test", version.ref = "coil" }
|
||||
compound = { module = "io.element.android:compound-android", version = "0.0.1" }
|
||||
datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "datetime" }
|
||||
serialization_json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization_json" }
|
||||
|
|
|
|||
|
|
@ -16,8 +16,6 @@
|
|||
|
||||
package io.element.android.libraries.androidutils.system
|
||||
|
||||
import android.annotation.TargetApi
|
||||
import android.app.Activity
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
|
|
@ -104,19 +102,6 @@ fun Context.openAppSettingsPage(
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows notification system settings for the given channel id.
|
||||
*/
|
||||
@TargetApi(Build.VERSION_CODES.O)
|
||||
fun Activity.startNotificationChannelSettingsIntent(channelID: String) {
|
||||
if (!supportNotificationChannels()) return
|
||||
val intent = Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS).apply {
|
||||
putExtra(Settings.EXTRA_APP_PACKAGE, packageName)
|
||||
putExtra(Settings.EXTRA_CHANNEL_ID, channelID)
|
||||
}
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
fun Context.startInstallFromSourceIntent(
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>,
|
||||
|
|
|
|||
|
|
@ -65,8 +65,10 @@ dependencies {
|
|||
testImplementation(libs.test.mockk)
|
||||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.test.turbine)
|
||||
testImplementation(libs.coil.test)
|
||||
testImplementation(libs.coroutines.test)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.tests.testutils)
|
||||
testImplementation(projects.services.appnavstate.test)
|
||||
testImplementation(projects.services.toolbox.impl)
|
||||
testImplementation(projects.services.toolbox.test)
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ import io.element.android.services.appnavstate.api.AppNavigationStateService
|
|||
import io.element.android.services.appnavstate.api.NavigationState
|
||||
import io.element.android.services.appnavstate.api.currentSessionId
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
|
@ -61,6 +62,8 @@ class DefaultNotificationDrawerManager @Inject constructor(
|
|||
private val buildMeta: BuildMeta,
|
||||
private val matrixClientProvider: MatrixClientProvider,
|
||||
) : NotificationDrawerManager {
|
||||
private var appNavigationStateObserver: Job? = null
|
||||
|
||||
/**
|
||||
* Lazily initializes the NotificationState as we rely on having a current session in order to fetch the persisted queue of events.
|
||||
*/
|
||||
|
|
@ -72,12 +75,17 @@ class DefaultNotificationDrawerManager @Inject constructor(
|
|||
|
||||
init {
|
||||
// Observe application state
|
||||
coroutineScope.launch {
|
||||
appNavigationStateObserver = coroutineScope.launch {
|
||||
appNavigationStateService.appNavigationState
|
||||
.collect { onAppNavigationStateChange(it.navigationState) }
|
||||
}
|
||||
}
|
||||
|
||||
// For test only
|
||||
fun destroy() {
|
||||
appNavigationStateObserver?.cancel()
|
||||
}
|
||||
|
||||
private var currentAppNavigationState: NavigationState? = null
|
||||
|
||||
private fun onAppNavigationStateChange(navigationState: NavigationState) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,94 @@
|
|||
/*
|
||||
* Copyright (c) 2021 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.push.impl.notifications
|
||||
|
||||
import android.content.Context
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.libraries.androidutils.file.EncryptedFileFactory
|
||||
import io.element.android.libraries.androidutils.file.safeDelete
|
||||
import io.element.android.libraries.core.data.tryOrNull
|
||||
import io.element.android.libraries.core.log.logger.LoggerTag
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.di.ApplicationContext
|
||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.io.ObjectInputStream
|
||||
import java.io.ObjectOutputStream
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val ROOMS_NOTIFICATIONS_FILE_NAME_LEGACY = "im.vector.notifications.cache"
|
||||
private const val FILE_NAME = "notifications.bin"
|
||||
|
||||
private val loggerTag = LoggerTag("NotificationEventPersistence", LoggerTag.NotificationLoggerTag)
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultNotificationEventPersistence @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
) : NotificationEventPersistence {
|
||||
private val file by lazy {
|
||||
deleteLegacyFileIfAny()
|
||||
context.getDatabasePath(FILE_NAME)
|
||||
}
|
||||
|
||||
private val encryptedFile by lazy {
|
||||
EncryptedFileFactory(context).create(file)
|
||||
}
|
||||
|
||||
override fun loadEvents(factory: (List<NotifiableEvent>) -> NotificationEventQueue): NotificationEventQueue {
|
||||
val rawEvents: ArrayList<NotifiableEvent>? = file
|
||||
.takeIf { it.exists() }
|
||||
?.let {
|
||||
try {
|
||||
encryptedFile.openFileInput().use { fis ->
|
||||
ObjectInputStream(fis).use { ois ->
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
ois.readObject() as? ArrayList<NotifiableEvent>
|
||||
}
|
||||
}.also {
|
||||
Timber.tag(loggerTag.value).d("Deserializing ${it?.size} NotifiableEvent(s)")
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Timber.tag(loggerTag.value).e(e, "## Failed to load cached notification info")
|
||||
null
|
||||
}
|
||||
}
|
||||
return factory(rawEvents.orEmpty())
|
||||
}
|
||||
|
||||
override fun persistEvents(queuedEvents: NotificationEventQueue) {
|
||||
Timber.tag(loggerTag.value).d("Serializing ${queuedEvents.rawEvents().size} NotifiableEvent(s)")
|
||||
// Always delete file before writing, or encryptedFile.openFileOutput() will throw
|
||||
file.safeDelete()
|
||||
if (queuedEvents.isEmpty()) return
|
||||
try {
|
||||
encryptedFile.openFileOutput().use { fos ->
|
||||
ObjectOutputStream(fos).use { oos ->
|
||||
oos.writeObject(queuedEvents.rawEvents())
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Timber.tag(loggerTag.value).e(e, "## Failed to save cached notification info")
|
||||
}
|
||||
}
|
||||
|
||||
private fun deleteLegacyFileIfAny() {
|
||||
tryOrNull {
|
||||
File(context.applicationContext.cacheDir, ROOMS_NOTIFICATIONS_FILE_NAME_LEGACY).delete()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -27,11 +27,13 @@ import coil.transform.CircleCropTransformation
|
|||
import io.element.android.libraries.di.ApplicationContext
|
||||
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||
import io.element.android.libraries.matrix.ui.media.MediaRequestData
|
||||
import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
class NotificationBitmapLoader @Inject constructor(
|
||||
@ApplicationContext private val context: Context
|
||||
@ApplicationContext private val context: Context,
|
||||
private val sdkIntProvider: BuildVersionSdkIntProvider,
|
||||
) {
|
||||
|
||||
/**
|
||||
|
|
@ -65,7 +67,7 @@ class NotificationBitmapLoader @Inject constructor(
|
|||
* @param path mxc url
|
||||
*/
|
||||
suspend fun getUserIcon(path: String?): IconCompat? {
|
||||
if (path == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
|
||||
if (path == null || sdkIntProvider.get() < Build.VERSION_CODES.P) {
|
||||
return null
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2021 New Vector Ltd
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
|
@ -16,76 +16,9 @@
|
|||
|
||||
package io.element.android.libraries.push.impl.notifications
|
||||
|
||||
import android.content.Context
|
||||
import io.element.android.libraries.androidutils.file.EncryptedFileFactory
|
||||
import io.element.android.libraries.androidutils.file.safeDelete
|
||||
import io.element.android.libraries.core.data.tryOrNull
|
||||
import io.element.android.libraries.core.log.logger.LoggerTag
|
||||
import io.element.android.libraries.di.ApplicationContext
|
||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.io.ObjectInputStream
|
||||
import java.io.ObjectOutputStream
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val ROOMS_NOTIFICATIONS_FILE_NAME_LEGACY = "im.vector.notifications.cache"
|
||||
private const val FILE_NAME = "notifications.bin"
|
||||
|
||||
private val loggerTag = LoggerTag("NotificationEventPersistence", LoggerTag.NotificationLoggerTag)
|
||||
|
||||
class NotificationEventPersistence @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
) {
|
||||
private val file by lazy {
|
||||
deleteLegacyFileIfAny()
|
||||
context.getDatabasePath(FILE_NAME)
|
||||
}
|
||||
|
||||
private val encryptedFile by lazy {
|
||||
EncryptedFileFactory(context).create(file)
|
||||
}
|
||||
|
||||
fun loadEvents(factory: (List<NotifiableEvent>) -> NotificationEventQueue): NotificationEventQueue {
|
||||
val rawEvents: ArrayList<NotifiableEvent>? = file
|
||||
.takeIf { it.exists() }
|
||||
?.let {
|
||||
try {
|
||||
encryptedFile.openFileInput().use { fis ->
|
||||
ObjectInputStream(fis).use { ois ->
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
ois.readObject() as? ArrayList<NotifiableEvent>
|
||||
}
|
||||
}.also {
|
||||
Timber.tag(loggerTag.value).d("Deserializing ${it?.size} NotifiableEvent(s)")
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Timber.tag(loggerTag.value).e(e, "## Failed to load cached notification info")
|
||||
null
|
||||
}
|
||||
}
|
||||
return factory(rawEvents.orEmpty())
|
||||
}
|
||||
|
||||
fun persistEvents(queuedEvents: NotificationEventQueue) {
|
||||
Timber.tag(loggerTag.value).d("Serializing ${queuedEvents.rawEvents().size} NotifiableEvent(s)")
|
||||
// Always delete file before writing, or encryptedFile.openFileOutput() will throw
|
||||
file.safeDelete()
|
||||
if (queuedEvents.isEmpty()) return
|
||||
try {
|
||||
encryptedFile.openFileOutput().use { fos ->
|
||||
ObjectOutputStream(fos).use { oos ->
|
||||
oos.writeObject(queuedEvents.rawEvents())
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Timber.tag(loggerTag.value).e(e, "## Failed to save cached notification info")
|
||||
}
|
||||
}
|
||||
|
||||
private fun deleteLegacyFileIfAny() {
|
||||
tryOrNull {
|
||||
File(context.applicationContext.cacheDir, ROOMS_NOTIFICATIONS_FILE_NAME_LEGACY).delete()
|
||||
}
|
||||
}
|
||||
interface NotificationEventPersistence {
|
||||
fun loadEvents(factory: (List<NotifiableEvent>) -> NotificationEventQueue): NotificationEventQueue
|
||||
fun persistEvents(queuedEvents: NotificationEventQueue)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ import io.element.android.libraries.push.impl.notifications.model.NotifiableMess
|
|||
import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiableEvent
|
||||
import timber.log.Timber
|
||||
|
||||
data class NotificationEventQueue constructor(
|
||||
data class NotificationEventQueue(
|
||||
private val queue: MutableList<NotifiableEvent>,
|
||||
/**
|
||||
* An in memory FIFO cache of the seen events.
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ package io.element.android.libraries.push.impl.notifications
|
|||
import android.app.Notification
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.push.impl.notifications.factories.NotificationFactory
|
||||
import io.element.android.libraries.push.impl.notifications.factories.NotificationCreator
|
||||
import io.element.android.libraries.push.impl.notifications.model.FallbackNotifiableEvent
|
||||
import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent
|
||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
|
||||
|
|
@ -28,9 +28,8 @@ import javax.inject.Inject
|
|||
|
||||
private typealias ProcessedMessageEvents = List<ProcessedEvent<NotifiableMessageEvent>>
|
||||
|
||||
// TODO Find a better name, it clashes with io.element.android.libraries.push.impl.notifications.factories.NotificationFactory
|
||||
class NotificationFactory @Inject constructor(
|
||||
private val notificationFactory: NotificationFactory,
|
||||
private val notificationCreator: NotificationCreator,
|
||||
private val roomGroupMessageCreator: RoomGroupMessageCreator,
|
||||
private val summaryGroupMessageCreator: SummaryGroupMessageCreator
|
||||
) {
|
||||
|
|
@ -65,7 +64,7 @@ class NotificationFactory @Inject constructor(
|
|||
when (processed) {
|
||||
ProcessedEvent.Type.REMOVE -> OneShotNotification.Removed(key = event.roomId.value)
|
||||
ProcessedEvent.Type.KEEP -> OneShotNotification.Append(
|
||||
notificationFactory.createRoomInvitationNotification(event),
|
||||
notificationCreator.createRoomInvitationNotification(event),
|
||||
OneShotNotification.Append.Meta(
|
||||
key = event.roomId.value,
|
||||
summaryLine = event.description,
|
||||
|
|
@ -83,7 +82,7 @@ class NotificationFactory @Inject constructor(
|
|||
when (processed) {
|
||||
ProcessedEvent.Type.REMOVE -> OneShotNotification.Removed(key = event.eventId.value)
|
||||
ProcessedEvent.Type.KEEP -> OneShotNotification.Append(
|
||||
notificationFactory.createSimpleEventNotification(event),
|
||||
notificationCreator.createSimpleEventNotification(event),
|
||||
OneShotNotification.Append.Meta(
|
||||
key = event.eventId.value,
|
||||
summaryLine = event.description,
|
||||
|
|
@ -100,7 +99,7 @@ class NotificationFactory @Inject constructor(
|
|||
when (processed) {
|
||||
ProcessedEvent.Type.REMOVE -> OneShotNotification.Removed(key = event.eventId.value)
|
||||
ProcessedEvent.Type.KEEP -> OneShotNotification.Append(
|
||||
notificationFactory.createFallbackNotification(event),
|
||||
notificationCreator.createFallbackNotification(event),
|
||||
OneShotNotification.Append.Meta(
|
||||
key = event.eventId.value,
|
||||
summaryLine = event.description.orEmpty(),
|
||||
|
|
|
|||
|
|
@ -27,16 +27,15 @@ import io.element.android.libraries.matrix.api.core.RoomId
|
|||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.push.impl.R
|
||||
import io.element.android.libraries.push.impl.notifications.debug.annotateForDebug
|
||||
import io.element.android.libraries.push.impl.notifications.factories.NotificationFactory
|
||||
import io.element.android.libraries.push.impl.notifications.factories.NotificationCreator
|
||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
|
||||
import io.element.android.services.toolbox.api.strings.StringProvider
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
class RoomGroupMessageCreator @Inject constructor(
|
||||
private val bitmapLoader: NotificationBitmapLoader,
|
||||
private val stringProvider: StringProvider,
|
||||
private val notificationFactory: NotificationFactory
|
||||
private val notificationCreator: NotificationCreator
|
||||
) {
|
||||
|
||||
suspend fun createRoomMessage(
|
||||
|
|
@ -78,7 +77,7 @@ class RoomGroupMessageCreator @Inject constructor(
|
|||
shouldBing = events.any { it.noisy }
|
||||
)
|
||||
return RoomNotification.Message(
|
||||
notificationFactory.createMessagesListNotification(
|
||||
notificationCreator.createMessagesListNotification(
|
||||
style,
|
||||
RoomEventGroupInfo(
|
||||
sessionId = currentUser.userId,
|
||||
|
|
@ -133,22 +132,16 @@ class RoomGroupMessageCreator @Inject constructor(
|
|||
}
|
||||
|
||||
private fun createRoomMessagesGroupSummaryLine(events: List<NotifiableMessageEvent>, roomName: String, roomIsDirect: Boolean): CharSequence {
|
||||
return try {
|
||||
when (events.size) {
|
||||
1 -> createFirstMessageSummaryLine(events.first(), roomName, roomIsDirect)
|
||||
else -> {
|
||||
stringProvider.getQuantityString(
|
||||
R.plurals.notification_compat_summary_line_for_room,
|
||||
events.size,
|
||||
roomName,
|
||||
events.size
|
||||
)
|
||||
}
|
||||
return when (events.size) {
|
||||
1 -> createFirstMessageSummaryLine(events.first(), roomName, roomIsDirect)
|
||||
else -> {
|
||||
stringProvider.getQuantityString(
|
||||
R.plurals.notification_compat_summary_line_for_room,
|
||||
events.size,
|
||||
roomName,
|
||||
events.size
|
||||
)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
// String not found or bad format
|
||||
Timber.v("%%%%%%%% REFRESH NOTIFICATION DRAWER failed to resolve string")
|
||||
roomName
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -166,7 +159,7 @@ class RoomGroupMessageCreator @Inject constructor(
|
|||
inSpans(StyleSpan(Typeface.BOLD)) {
|
||||
append(roomName)
|
||||
append(": ")
|
||||
event.senderName
|
||||
append(event.senderName)
|
||||
append(" ")
|
||||
}
|
||||
append(event.description)
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import androidx.core.app.NotificationCompat
|
|||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.push.impl.R
|
||||
import io.element.android.libraries.push.impl.notifications.debug.annotateForDebug
|
||||
import io.element.android.libraries.push.impl.notifications.factories.NotificationFactory
|
||||
import io.element.android.libraries.push.impl.notifications.factories.NotificationCreator
|
||||
import io.element.android.services.toolbox.api.strings.StringProvider
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
@ -41,7 +41,7 @@ import javax.inject.Inject
|
|||
*/
|
||||
class SummaryGroupMessageCreator @Inject constructor(
|
||||
private val stringProvider: StringProvider,
|
||||
private val notificationFactory: NotificationFactory,
|
||||
private val notificationCreator: NotificationCreator,
|
||||
) {
|
||||
|
||||
fun createSummaryNotification(
|
||||
|
|
@ -77,7 +77,7 @@ class SummaryGroupMessageCreator @Inject constructor(
|
|||
// Use account name now, for multi-session
|
||||
.setSummaryText(currentUser.userId.value.annotateForDebug(44))
|
||||
return if (useCompleteNotificationFormat) {
|
||||
notificationFactory.createSummaryListNotification(
|
||||
notificationCreator.createSummaryListNotification(
|
||||
currentUser,
|
||||
summaryInboxStyle,
|
||||
sumTitle,
|
||||
|
|
@ -170,7 +170,7 @@ class SummaryGroupMessageCreator @Inject constructor(
|
|||
messageStr
|
||||
}
|
||||
}
|
||||
return notificationFactory.createSummaryListNotification(
|
||||
return notificationCreator.createSummaryListNotification(
|
||||
currentUser = currentUser,
|
||||
style = null,
|
||||
compatSummary = privacyTitle,
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@
|
|||
|
||||
package io.element.android.libraries.push.impl.notifications.channels
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
|
|
@ -24,7 +23,6 @@ import android.os.Build
|
|||
import androidx.annotation.ChecksSdkIntAtLeast
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import io.element.android.libraries.androidutils.system.startNotificationChannelSettingsIntent
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.di.ApplicationContext
|
||||
import io.element.android.libraries.di.SingleIn
|
||||
|
|
@ -163,17 +161,5 @@ class NotificationChannels @Inject constructor(
|
|||
|
||||
@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.O)
|
||||
private fun supportNotificationChannels() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
|
||||
|
||||
fun openSystemSettingsForSilentCategory(activity: Activity) {
|
||||
activity.startNotificationChannelSettingsIntent(SILENT_NOTIFICATION_CHANNEL_ID)
|
||||
}
|
||||
|
||||
fun openSystemSettingsForNoisyCategory(activity: Activity) {
|
||||
activity.startNotificationChannelSettingsIntent(NOISY_NOTIFICATION_CHANNEL_ID)
|
||||
}
|
||||
|
||||
fun openSystemSettingsForCallCategory(activity: Activity) {
|
||||
activity.startNotificationChannelSettingsIntent(CALL_NOTIFICATION_CHANNEL_ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiab
|
|||
import io.element.android.services.toolbox.api.strings.StringProvider
|
||||
import javax.inject.Inject
|
||||
|
||||
class NotificationFactory @Inject constructor(
|
||||
class NotificationCreator @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val notificationChannels: NotificationChannels,
|
||||
private val stringProvider: StringProvider,
|
||||
|
|
@ -0,0 +1,134 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.push.impl.notifications
|
||||
|
||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
import io.element.android.libraries.matrix.test.A_SPACE_ID
|
||||
import io.element.android.libraries.matrix.test.A_THREAD_ID
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClientProvider
|
||||
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||
import io.element.android.libraries.push.impl.notifications.fake.FakeAndroidNotificationFactory
|
||||
import io.element.android.libraries.push.impl.notifications.fake.FakeRoomGroupMessageCreator
|
||||
import io.element.android.libraries.push.impl.notifications.fake.FakeSummaryGroupMessageCreator
|
||||
import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent
|
||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
|
||||
import io.element.android.services.appnavstate.api.AppNavigationState
|
||||
import io.element.android.services.appnavstate.api.AppNavigationStateService
|
||||
import io.element.android.services.appnavstate.api.NavigationState
|
||||
import io.element.android.services.appnavstate.test.FakeAppNavigationStateService
|
||||
import io.element.android.services.appnavstate.test.aNavigationState
|
||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.runCurrent
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.RuntimeEnvironment
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
class DefaultNotificationDrawerManagerTest {
|
||||
@Test
|
||||
fun `clearAllEvents should have no effect when queue is empty`() = runTest {
|
||||
val defaultNotificationDrawerManager = createDefaultNotificationDrawerManager()
|
||||
defaultNotificationDrawerManager.clearAllEvents(A_SESSION_ID)
|
||||
defaultNotificationDrawerManager.destroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `cover all APIs`() = runTest {
|
||||
// For now just call all the API. Later, add more valuable tests.
|
||||
val defaultNotificationDrawerManager = createDefaultNotificationDrawerManager()
|
||||
defaultNotificationDrawerManager.notificationStyleChanged()
|
||||
defaultNotificationDrawerManager.clearAllMessagesEvents(A_SESSION_ID, doRender = true)
|
||||
defaultNotificationDrawerManager.clearAllMessagesEvents(A_SESSION_ID, doRender = false)
|
||||
defaultNotificationDrawerManager.clearEvent(A_SESSION_ID, AN_EVENT_ID, doRender = true)
|
||||
defaultNotificationDrawerManager.clearEvent(A_SESSION_ID, AN_EVENT_ID, doRender = false)
|
||||
defaultNotificationDrawerManager.clearMessagesForRoom(A_SESSION_ID, A_ROOM_ID, doRender = true)
|
||||
defaultNotificationDrawerManager.clearMessagesForRoom(A_SESSION_ID, A_ROOM_ID, doRender = false)
|
||||
defaultNotificationDrawerManager.clearMembershipNotificationForSession(A_SESSION_ID)
|
||||
defaultNotificationDrawerManager.clearMembershipNotificationForRoom(A_SESSION_ID, A_ROOM_ID, doRender = true)
|
||||
defaultNotificationDrawerManager.clearMembershipNotificationForRoom(A_SESSION_ID, A_ROOM_ID, doRender = false)
|
||||
defaultNotificationDrawerManager.onNotifiableEventReceived(aNotifiableMessageEvent())
|
||||
// Add the same Event again (will be ignored)
|
||||
defaultNotificationDrawerManager.onNotifiableEventReceived(aNotifiableMessageEvent())
|
||||
defaultNotificationDrawerManager.destroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `react to applicationStateChange`() = runTest {
|
||||
// For now just call all the API. Later, add more valuable tests.
|
||||
val appNavigationStateFlow: MutableStateFlow<AppNavigationState> = MutableStateFlow(
|
||||
AppNavigationState(
|
||||
navigationState = NavigationState.Root,
|
||||
isInForeground = true,
|
||||
)
|
||||
)
|
||||
val appNavigationStateService = FakeAppNavigationStateService(appNavigationState = appNavigationStateFlow)
|
||||
val defaultNotificationDrawerManager = createDefaultNotificationDrawerManager(
|
||||
appNavigationStateService = appNavigationStateService
|
||||
)
|
||||
appNavigationStateFlow.emit(AppNavigationState(aNavigationState(), isInForeground = true))
|
||||
runCurrent()
|
||||
appNavigationStateFlow.emit(AppNavigationState(aNavigationState(A_SESSION_ID), isInForeground = true))
|
||||
runCurrent()
|
||||
appNavigationStateFlow.emit(AppNavigationState(aNavigationState(A_SESSION_ID, A_SPACE_ID), isInForeground = true))
|
||||
runCurrent()
|
||||
appNavigationStateFlow.emit(AppNavigationState(aNavigationState(A_SESSION_ID, A_SPACE_ID, A_ROOM_ID), isInForeground = true))
|
||||
runCurrent()
|
||||
appNavigationStateFlow.emit(AppNavigationState(aNavigationState(A_SESSION_ID, A_SPACE_ID, A_ROOM_ID, A_THREAD_ID), isInForeground = true))
|
||||
runCurrent()
|
||||
// Like a user sign out
|
||||
appNavigationStateFlow.emit(AppNavigationState(aNavigationState(), isInForeground = true))
|
||||
runCurrent()
|
||||
defaultNotificationDrawerManager.destroy()
|
||||
}
|
||||
|
||||
private fun TestScope.createDefaultNotificationDrawerManager(
|
||||
appNavigationStateService: AppNavigationStateService = FakeAppNavigationStateService(),
|
||||
initialData: List<NotifiableEvent> = emptyList()
|
||||
): DefaultNotificationDrawerManager {
|
||||
val context = RuntimeEnvironment.getApplication()
|
||||
return DefaultNotificationDrawerManager(
|
||||
notifiableEventProcessor = NotifiableEventProcessor(
|
||||
outdatedDetector = OutdatedEventDetector(),
|
||||
appNavigationStateService = appNavigationStateService
|
||||
),
|
||||
notificationRenderer = NotificationRenderer(
|
||||
NotificationIdProvider(),
|
||||
NotificationDisplayer(context),
|
||||
NotificationFactory(
|
||||
FakeAndroidNotificationFactory().instance,
|
||||
FakeRoomGroupMessageCreator().instance,
|
||||
FakeSummaryGroupMessageCreator().instance,
|
||||
)
|
||||
),
|
||||
notificationEventPersistence = InMemoryNotificationEventPersistence(initialData = initialData),
|
||||
filteredEventDetector = FilteredEventDetector(),
|
||||
appNavigationStateService = appNavigationStateService,
|
||||
coroutineScope = this,
|
||||
dispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true),
|
||||
buildMeta = aBuildMeta(),
|
||||
matrixClientProvider = FakeMatrixClientProvider(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.push.impl.notifications
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.core.cache.CircularCache
|
||||
import io.element.android.libraries.push.impl.notifications.fixtures.aSimpleNotifiableEvent
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.RuntimeEnvironment
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
class DefaultNotificationEventPersistenceTest {
|
||||
@Test
|
||||
fun `loadEvents should return empty NotificationEventQueue`() {
|
||||
val sut = createDefaultNotificationEventPersistence()
|
||||
val result = sut.loadEvents(
|
||||
factory = { rawEvents ->
|
||||
NotificationEventQueue(rawEvents.toMutableList(), seenEventIds = CircularCache.create(cacheSize = 25))
|
||||
}
|
||||
)
|
||||
assertThat(result.isEmpty()).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `after persisting NotificationEventQueue, loadEvents should return non-empty NotificationEventQueue`() {
|
||||
val sut = createDefaultNotificationEventPersistence()
|
||||
val notificationEventQueue = NotificationEventQueue(mutableListOf(), seenEventIds = CircularCache.create(cacheSize = 25))
|
||||
// First persist an empty queue
|
||||
sut.persistEvents(notificationEventQueue)
|
||||
// Add an event
|
||||
notificationEventQueue.add(aSimpleNotifiableEvent())
|
||||
// Persist
|
||||
// Note: is cannot work because AndroidKeyStore is not available. But we check that the code does
|
||||
// not crash.
|
||||
sut.persistEvents(notificationEventQueue)
|
||||
sut.loadEvents(
|
||||
factory = { rawEvents ->
|
||||
NotificationEventQueue(rawEvents.toMutableList(), seenEventIds = CircularCache.create(cacheSize = 25))
|
||||
}
|
||||
)
|
||||
// assertThat(result.isEmpty()).isFalse()
|
||||
}
|
||||
|
||||
private fun createDefaultNotificationEventPersistence(): DefaultNotificationEventPersistence {
|
||||
val context = RuntimeEnvironment.getApplication()
|
||||
return DefaultNotificationEventPersistence(context)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.push.impl.notifications
|
||||
|
||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
|
||||
|
||||
class InMemoryNotificationEventPersistence(
|
||||
initialData: List<NotifiableEvent> = emptyList()
|
||||
) : NotificationEventPersistence {
|
||||
private var data: List<NotifiableEvent> = initialData
|
||||
|
||||
override fun loadEvents(factory: (List<NotifiableEvent>) -> NotificationEventQueue): NotificationEventQueue {
|
||||
return factory(data)
|
||||
}
|
||||
|
||||
override fun persistEvents(queuedEvents: NotificationEventQueue) {
|
||||
data = queuedEvents.rawEvents()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,280 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.push.impl.notifications
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.os.Build
|
||||
import coil.Coil
|
||||
import coil.ImageLoader
|
||||
import coil.annotation.ExperimentalCoilApi
|
||||
import coil.test.FakeImageLoaderEngine
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
||||
import io.element.android.libraries.matrix.ui.media.MediaRequestData
|
||||
import io.element.android.libraries.push.impl.notifications.factories.createNotificationCreator
|
||||
import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent
|
||||
import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider
|
||||
import io.element.android.services.toolbox.impl.strings.AndroidStringProvider
|
||||
import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvider
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.RuntimeEnvironment
|
||||
|
||||
private const val A_TIMESTAMP = 6480L
|
||||
private const val A_ROOM_AVATAR = "mxc://roomAvatar"
|
||||
private const val A_USER_AVATAR_1 = "mxc://userAvatar1"
|
||||
private const val A_USER_AVATAR_2 = "mxc://userAvatar2"
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
class RoomGroupMessageCreatorTest {
|
||||
@Test
|
||||
fun `test createRoomMessage with one Event`() = runTest {
|
||||
val sut = createRoomGroupMessageCreator()
|
||||
val result = sut.createRoomMessage(
|
||||
currentUser = aMatrixUser(),
|
||||
events = listOf(
|
||||
aNotifiableMessageEvent(timestamp = A_TIMESTAMP).copy(
|
||||
imageUriString = "aUri",
|
||||
)
|
||||
),
|
||||
roomId = A_ROOM_ID,
|
||||
)
|
||||
val resultMetaWithoutFormatting = result.meta.copy(
|
||||
summaryLine = result.meta.summaryLine.toString()
|
||||
)
|
||||
assertThat(resultMetaWithoutFormatting).isEqualTo(
|
||||
RoomNotification.Message.Meta(
|
||||
roomId = A_ROOM_ID,
|
||||
summaryLine = "room-name: sender-name message-body",
|
||||
messageCount = 1,
|
||||
latestTimestamp = A_TIMESTAMP,
|
||||
shouldBing = false,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test createRoomMessage with one noisy Event`() = runTest {
|
||||
val sut = createRoomGroupMessageCreator()
|
||||
val result = sut.createRoomMessage(
|
||||
currentUser = aMatrixUser(),
|
||||
events = listOf(
|
||||
aNotifiableMessageEvent(timestamp = A_TIMESTAMP).copy(
|
||||
noisy = true,
|
||||
)
|
||||
),
|
||||
roomId = A_ROOM_ID,
|
||||
)
|
||||
val resultMetaWithoutFormatting = result.meta.copy(
|
||||
summaryLine = result.meta.summaryLine.toString()
|
||||
)
|
||||
assertThat(resultMetaWithoutFormatting).isEqualTo(
|
||||
RoomNotification.Message.Meta(
|
||||
roomId = A_ROOM_ID,
|
||||
summaryLine = "room-name: sender-name message-body",
|
||||
messageCount = 1,
|
||||
latestTimestamp = A_TIMESTAMP,
|
||||
shouldBing = true,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test createRoomMessage with room avatar and sender avatar android O`() {
|
||||
`test createRoomMessage with room avatar and sender avatar`(
|
||||
api = Build.VERSION_CODES.O,
|
||||
// Only the Room avatar is loaded
|
||||
expectedCoilRequests = listOf(
|
||||
MediaRequestData(
|
||||
source = MediaSource(url = A_ROOM_AVATAR),
|
||||
kind = MediaRequestData.Kind.Thumbnail(1024)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoilApi::class)
|
||||
@Test
|
||||
fun `test createRoomMessage with room avatar and sender avatar android P`() = runTest {
|
||||
`test createRoomMessage with room avatar and sender avatar`(
|
||||
api = Build.VERSION_CODES.P,
|
||||
// Room and user avatar are loaded
|
||||
expectedCoilRequests = listOf(
|
||||
MediaRequestData(
|
||||
source = MediaSource(url = A_USER_AVATAR_1),
|
||||
kind = MediaRequestData.Kind.Thumbnail(1024)
|
||||
),
|
||||
MediaRequestData(
|
||||
source = MediaSource(url = A_USER_AVATAR_2),
|
||||
kind = MediaRequestData.Kind.Thumbnail(1024)
|
||||
),
|
||||
MediaRequestData(
|
||||
source = MediaSource(url = A_ROOM_AVATAR),
|
||||
kind = MediaRequestData.Kind.Thumbnail(1024)
|
||||
),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoilApi::class)
|
||||
private fun `test createRoomMessage with room avatar and sender avatar`(
|
||||
api: Int,
|
||||
expectedCoilRequests: List<Any>,
|
||||
) = runTest {
|
||||
val coilRequests = mutableListOf<Any>()
|
||||
val engine = FakeImageLoaderEngine.Builder()
|
||||
.intercept(
|
||||
predicate = {
|
||||
coilRequests.add(it)
|
||||
true
|
||||
},
|
||||
drawable = ColorDrawable(Color.BLUE)
|
||||
)
|
||||
.build()
|
||||
val imageLoader = ImageLoader.Builder(RuntimeEnvironment.getApplication())
|
||||
.components { add(engine) }
|
||||
.build()
|
||||
Coil.setImageLoader(imageLoader)
|
||||
val sut = createRoomGroupMessageCreator(
|
||||
sdkIntProvider = FakeBuildVersionSdkIntProvider(api)
|
||||
)
|
||||
val result = sut.createRoomMessage(
|
||||
currentUser = aMatrixUser(
|
||||
// Some user avatar
|
||||
avatarUrl = A_USER_AVATAR_1,
|
||||
),
|
||||
events = listOf(
|
||||
aNotifiableMessageEvent(timestamp = A_TIMESTAMP).copy(
|
||||
roomAvatarPath = A_ROOM_AVATAR,
|
||||
senderAvatarPath = A_USER_AVATAR_2,
|
||||
)
|
||||
),
|
||||
roomId = A_ROOM_ID,
|
||||
)
|
||||
val resultMetaWithoutFormatting = result.meta.copy(
|
||||
summaryLine = result.meta.summaryLine.toString()
|
||||
)
|
||||
assertThat(resultMetaWithoutFormatting).isEqualTo(
|
||||
RoomNotification.Message.Meta(
|
||||
roomId = A_ROOM_ID,
|
||||
summaryLine = "room-name: sender-name message-body",
|
||||
messageCount = 1,
|
||||
latestTimestamp = A_TIMESTAMP,
|
||||
shouldBing = false,
|
||||
)
|
||||
)
|
||||
assertThat(coilRequests.toList()).isEqualTo(expectedCoilRequests)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test createRoomMessage with two Events`() = runTest {
|
||||
val sut = createRoomGroupMessageCreator()
|
||||
val result = sut.createRoomMessage(
|
||||
currentUser = aMatrixUser(),
|
||||
events = listOf(
|
||||
aNotifiableMessageEvent(timestamp = A_TIMESTAMP),
|
||||
aNotifiableMessageEvent(timestamp = A_TIMESTAMP + 10),
|
||||
),
|
||||
roomId = A_ROOM_ID,
|
||||
)
|
||||
val resultMetaWithoutFormatting = result.meta.copy(
|
||||
summaryLine = result.meta.summaryLine.toString()
|
||||
)
|
||||
assertThat(resultMetaWithoutFormatting).isEqualTo(
|
||||
RoomNotification.Message.Meta(
|
||||
roomId = A_ROOM_ID,
|
||||
summaryLine = "room-name: 2 messages",
|
||||
messageCount = 2,
|
||||
latestTimestamp = A_TIMESTAMP + 10,
|
||||
shouldBing = false,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test createRoomMessage with smart reply error`() = runTest {
|
||||
val sut = createRoomGroupMessageCreator()
|
||||
val result = sut.createRoomMessage(
|
||||
currentUser = aMatrixUser(),
|
||||
events = listOf(
|
||||
aNotifiableMessageEvent(timestamp = A_TIMESTAMP).copy(
|
||||
outGoingMessage = true,
|
||||
outGoingMessageFailed = true,
|
||||
),
|
||||
),
|
||||
roomId = A_ROOM_ID,
|
||||
)
|
||||
val resultMetaWithoutFormatting = result.meta.copy(
|
||||
summaryLine = result.meta.summaryLine.toString()
|
||||
)
|
||||
assertThat(resultMetaWithoutFormatting).isEqualTo(
|
||||
RoomNotification.Message.Meta(
|
||||
roomId = A_ROOM_ID,
|
||||
summaryLine = "room-name: sender-name message-body",
|
||||
messageCount = 0,
|
||||
latestTimestamp = A_TIMESTAMP,
|
||||
shouldBing = false,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test createRoomMessage for direct room`() = runTest {
|
||||
val sut = createRoomGroupMessageCreator()
|
||||
val result = sut.createRoomMessage(
|
||||
currentUser = aMatrixUser(),
|
||||
events = listOf(
|
||||
aNotifiableMessageEvent(timestamp = A_TIMESTAMP).copy(
|
||||
roomIsDirect = true,
|
||||
),
|
||||
),
|
||||
roomId = A_ROOM_ID,
|
||||
)
|
||||
val resultMetaWithoutFormatting = result.meta.copy(
|
||||
summaryLine = result.meta.summaryLine.toString()
|
||||
)
|
||||
assertThat(resultMetaWithoutFormatting).isEqualTo(
|
||||
RoomNotification.Message.Meta(
|
||||
roomId = A_ROOM_ID,
|
||||
summaryLine = "sender-name: message-body",
|
||||
messageCount = 1,
|
||||
latestTimestamp = A_TIMESTAMP,
|
||||
shouldBing = false,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun createRoomGroupMessageCreator(
|
||||
sdkIntProvider: BuildVersionSdkIntProvider = FakeBuildVersionSdkIntProvider(Build.VERSION_CODES.O),
|
||||
): RoomGroupMessageCreator {
|
||||
val context = RuntimeEnvironment.getApplication() as Context
|
||||
return RoomGroupMessageCreator(
|
||||
notificationCreator = createNotificationCreator(),
|
||||
bitmapLoader = NotificationBitmapLoader(
|
||||
context = RuntimeEnvironment.getApplication(),
|
||||
sdkIntProvider = sdkIntProvider,
|
||||
),
|
||||
stringProvider = AndroidStringProvider(context.resources)
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.push.impl.notifications.factories
|
||||
|
||||
import android.content.Intent
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.core.ThreadId
|
||||
import io.element.android.libraries.push.impl.intent.IntentProvider
|
||||
|
||||
class FakeIntentProvider : IntentProvider {
|
||||
override fun getViewRoomIntent(sessionId: SessionId, roomId: RoomId?, threadId: ThreadId?) = Intent()
|
||||
|
||||
override fun getInviteListIntent(sessionId: SessionId) = Intent()
|
||||
}
|
||||
|
|
@ -0,0 +1,314 @@
|
|||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.push.impl.notifications.factories
|
||||
|
||||
import android.app.Notification
|
||||
import android.content.Context
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.Person
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
import io.element.android.libraries.matrix.test.A_THREAD_ID
|
||||
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
||||
import io.element.android.libraries.push.impl.notifications.NotificationActionIds
|
||||
import io.element.android.libraries.push.impl.notifications.RoomEventGroupInfo
|
||||
import io.element.android.libraries.push.impl.notifications.channels.NotificationChannels
|
||||
import io.element.android.libraries.push.impl.notifications.factories.action.MarkAsReadActionFactory
|
||||
import io.element.android.libraries.push.impl.notifications.factories.action.QuickReplyActionFactory
|
||||
import io.element.android.libraries.push.impl.notifications.model.FallbackNotifiableEvent
|
||||
import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent
|
||||
import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiableEvent
|
||||
import io.element.android.services.toolbox.test.strings.FakeStringProvider
|
||||
import io.element.android.services.toolbox.test.systemclock.A_FAKE_TIMESTAMP
|
||||
import io.element.android.services.toolbox.test.systemclock.FakeSystemClock
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.RuntimeEnvironment
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
class NotificationCreatorTest {
|
||||
@Test
|
||||
fun `test createDiagnosticNotification`() {
|
||||
val sut = createNotificationCreator()
|
||||
val result = sut.createDiagnosticNotification()
|
||||
result.commonAssertions(
|
||||
expectedGroup = null,
|
||||
expectedCategory = NotificationCompat.CATEGORY_STATUS,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test createFallbackNotification`() {
|
||||
val sut = createNotificationCreator()
|
||||
val result = sut.createFallbackNotification(
|
||||
FallbackNotifiableEvent(
|
||||
sessionId = A_SESSION_ID,
|
||||
roomId = A_ROOM_ID,
|
||||
eventId = AN_EVENT_ID,
|
||||
editedEventId = null,
|
||||
description = "description",
|
||||
canBeReplaced = false,
|
||||
isRedacted = false,
|
||||
isUpdated = false,
|
||||
timestamp = A_FAKE_TIMESTAMP,
|
||||
)
|
||||
)
|
||||
result.commonAssertions(
|
||||
expectedCategory = null,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test createSimpleEventNotification`() {
|
||||
val sut = createNotificationCreator()
|
||||
val result = sut.createSimpleEventNotification(
|
||||
SimpleNotifiableEvent(
|
||||
sessionId = A_SESSION_ID,
|
||||
roomId = A_ROOM_ID,
|
||||
eventId = AN_EVENT_ID,
|
||||
editedEventId = null,
|
||||
noisy = false,
|
||||
title = "title",
|
||||
description = "description",
|
||||
type = null,
|
||||
timestamp = A_FAKE_TIMESTAMP,
|
||||
soundName = null,
|
||||
canBeReplaced = false,
|
||||
isRedacted = false,
|
||||
isUpdated = false,
|
||||
)
|
||||
)
|
||||
result.commonAssertions(
|
||||
expectedCategory = null,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test createSimpleEventNotification noisy`() {
|
||||
val sut = createNotificationCreator()
|
||||
val result = sut.createSimpleEventNotification(
|
||||
SimpleNotifiableEvent(
|
||||
sessionId = A_SESSION_ID,
|
||||
roomId = A_ROOM_ID,
|
||||
eventId = AN_EVENT_ID,
|
||||
editedEventId = null,
|
||||
noisy = true,
|
||||
title = "title",
|
||||
description = "description",
|
||||
type = null,
|
||||
timestamp = A_FAKE_TIMESTAMP,
|
||||
soundName = null,
|
||||
canBeReplaced = false,
|
||||
isRedacted = false,
|
||||
isUpdated = false,
|
||||
)
|
||||
)
|
||||
result.commonAssertions(
|
||||
expectedCategory = null,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test createRoomInvitationNotification`() {
|
||||
val sut = createNotificationCreator()
|
||||
val result = sut.createRoomInvitationNotification(
|
||||
InviteNotifiableEvent(
|
||||
sessionId = A_SESSION_ID,
|
||||
roomId = A_ROOM_ID,
|
||||
eventId = AN_EVENT_ID,
|
||||
editedEventId = null,
|
||||
noisy = false,
|
||||
title = "title",
|
||||
description = "description",
|
||||
type = null,
|
||||
timestamp = A_FAKE_TIMESTAMP,
|
||||
soundName = null,
|
||||
canBeReplaced = false,
|
||||
isRedacted = false,
|
||||
isUpdated = false,
|
||||
roomName = "roomName",
|
||||
)
|
||||
)
|
||||
result.commonAssertions(
|
||||
expectedCategory = null,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test createRoomInvitationNotification noisy`() {
|
||||
val sut = createNotificationCreator()
|
||||
val result = sut.createRoomInvitationNotification(
|
||||
InviteNotifiableEvent(
|
||||
sessionId = A_SESSION_ID,
|
||||
roomId = A_ROOM_ID,
|
||||
eventId = AN_EVENT_ID,
|
||||
editedEventId = null,
|
||||
noisy = true,
|
||||
title = "title",
|
||||
description = "description",
|
||||
type = null,
|
||||
timestamp = A_FAKE_TIMESTAMP,
|
||||
soundName = null,
|
||||
canBeReplaced = false,
|
||||
isRedacted = false,
|
||||
isUpdated = false,
|
||||
roomName = "roomName",
|
||||
)
|
||||
)
|
||||
result.commonAssertions(
|
||||
expectedCategory = null,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test createSummaryListNotification`() {
|
||||
val sut = createNotificationCreator()
|
||||
val matrixUser = aMatrixUser()
|
||||
val result = sut.createSummaryListNotification(
|
||||
currentUser = matrixUser,
|
||||
style = null,
|
||||
compatSummary = "compatSummary",
|
||||
noisy = false,
|
||||
lastMessageTimestamp = 123_456L,
|
||||
)
|
||||
result.commonAssertions(
|
||||
expectedGroup = matrixUser.userId.value,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test createSummaryListNotification noisy`() {
|
||||
val sut = createNotificationCreator()
|
||||
val matrixUser = aMatrixUser()
|
||||
val result = sut.createSummaryListNotification(
|
||||
currentUser = matrixUser,
|
||||
style = null,
|
||||
compatSummary = "compatSummary",
|
||||
noisy = true,
|
||||
lastMessageTimestamp = 123_456L,
|
||||
)
|
||||
result.commonAssertions(
|
||||
expectedGroup = matrixUser.userId.value,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test createMessagesListNotification`() {
|
||||
val sut = createNotificationCreator()
|
||||
aMatrixUser()
|
||||
val result = sut.createMessagesListNotification(
|
||||
messageStyle = NotificationCompat.MessagingStyle(
|
||||
Person.Builder()
|
||||
.setName("name")
|
||||
.build()
|
||||
),
|
||||
roomInfo = RoomEventGroupInfo(
|
||||
sessionId = A_SESSION_ID,
|
||||
roomId = A_ROOM_ID,
|
||||
roomDisplayName = "roomDisplayName",
|
||||
isDirect = false,
|
||||
hasSmartReplyError = false,
|
||||
shouldBing = false,
|
||||
customSound = null,
|
||||
isUpdated = false,
|
||||
),
|
||||
threadId = null,
|
||||
largeIcon = null,
|
||||
lastMessageTimestamp = 123_456L,
|
||||
tickerText = "tickerText",
|
||||
)
|
||||
result.commonAssertions()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test createMessagesListNotification should bing and thread`() {
|
||||
val sut = createNotificationCreator()
|
||||
aMatrixUser()
|
||||
val result = sut.createMessagesListNotification(
|
||||
messageStyle = NotificationCompat.MessagingStyle(
|
||||
Person.Builder()
|
||||
.setName("name")
|
||||
.build()
|
||||
),
|
||||
roomInfo = RoomEventGroupInfo(
|
||||
sessionId = A_SESSION_ID,
|
||||
roomId = A_ROOM_ID,
|
||||
roomDisplayName = "roomDisplayName",
|
||||
isDirect = false,
|
||||
hasSmartReplyError = false,
|
||||
shouldBing = true,
|
||||
customSound = null,
|
||||
isUpdated = false,
|
||||
),
|
||||
threadId = A_THREAD_ID,
|
||||
largeIcon = null,
|
||||
lastMessageTimestamp = 123_456L,
|
||||
tickerText = "tickerText",
|
||||
)
|
||||
result.commonAssertions()
|
||||
}
|
||||
|
||||
private fun Notification.commonAssertions(
|
||||
expectedGroup: String? = A_SESSION_ID.value,
|
||||
expectedCategory: String? = NotificationCompat.CATEGORY_MESSAGE,
|
||||
) {
|
||||
assertThat(contentIntent).isNotNull()
|
||||
assertThat(group).isEqualTo(expectedGroup)
|
||||
assertThat(category).isEqualTo(expectedCategory)
|
||||
}
|
||||
}
|
||||
|
||||
fun createNotificationCreator(
|
||||
context: Context = RuntimeEnvironment.getApplication(),
|
||||
buildMeta: BuildMeta = aBuildMeta(),
|
||||
notificationChannels: NotificationChannels = createNotificationChannels()
|
||||
): NotificationCreator {
|
||||
return NotificationCreator(
|
||||
context = context,
|
||||
notificationChannels = notificationChannels,
|
||||
stringProvider = FakeStringProvider("test"),
|
||||
buildMeta = buildMeta,
|
||||
pendingIntentFactory = PendingIntentFactory(
|
||||
context,
|
||||
FakeIntentProvider(),
|
||||
FakeSystemClock(),
|
||||
NotificationActionIds(buildMeta),
|
||||
),
|
||||
markAsReadActionFactory = MarkAsReadActionFactory(
|
||||
context = context,
|
||||
actionIds = NotificationActionIds(buildMeta),
|
||||
stringProvider = FakeStringProvider("MarkAsReadActionFactory"),
|
||||
clock = FakeSystemClock(),
|
||||
),
|
||||
quickReplyActionFactory = QuickReplyActionFactory(
|
||||
context = context,
|
||||
actionIds = NotificationActionIds(buildMeta),
|
||||
stringProvider = FakeStringProvider("QuickReplyActionFactory"),
|
||||
clock = FakeSystemClock(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun createNotificationChannels(): NotificationChannels {
|
||||
val context = RuntimeEnvironment.getApplication()
|
||||
return NotificationChannels(context, FakeStringProvider(""))
|
||||
}
|
||||
|
|
@ -17,14 +17,14 @@
|
|||
package io.element.android.libraries.push.impl.notifications.fake
|
||||
|
||||
import android.app.Notification
|
||||
import io.element.android.libraries.push.impl.notifications.factories.NotificationFactory
|
||||
import io.element.android.libraries.push.impl.notifications.factories.NotificationCreator
|
||||
import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent
|
||||
import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiableEvent
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
|
||||
class FakeAndroidNotificationFactory {
|
||||
val instance = mockk<NotificationFactory>()
|
||||
val instance = mockk<NotificationCreator>()
|
||||
|
||||
fun givenCreateRoomInvitationNotificationFor(event: InviteNotifiableEvent): Notification {
|
||||
val mockNotification = mockk<Notification>()
|
||||
|
|
|
|||
|
|
@ -77,13 +77,14 @@ fun aNotifiableMessageEvent(
|
|||
roomId: RoomId = A_ROOM_ID,
|
||||
eventId: EventId = AN_EVENT_ID,
|
||||
threadId: ThreadId? = null,
|
||||
isRedacted: Boolean = false
|
||||
isRedacted: Boolean = false,
|
||||
timestamp: Long = 0,
|
||||
) = NotifiableMessageEvent(
|
||||
sessionId = sessionId,
|
||||
eventId = eventId,
|
||||
editedEventId = null,
|
||||
noisy = false,
|
||||
timestamp = 0,
|
||||
timestamp = timestamp,
|
||||
senderName = "sender-name",
|
||||
senderId = UserId("@sending-id:domain.com"),
|
||||
body = "message-body",
|
||||
|
|
|
|||
|
|
@ -20,23 +20,19 @@ import io.element.android.libraries.matrix.api.core.RoomId
|
|||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.core.SpaceId
|
||||
import io.element.android.libraries.matrix.api.core.ThreadId
|
||||
import io.element.android.services.appnavstate.api.NavigationState
|
||||
import io.element.android.services.appnavstate.api.AppNavigationStateService
|
||||
import io.element.android.services.appnavstate.api.AppNavigationState
|
||||
import io.element.android.services.appnavstate.api.AppNavigationStateService
|
||||
import io.element.android.services.appnavstate.api.NavigationState
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
class FakeAppNavigationStateService(
|
||||
private val fakeAppNavigationState: MutableStateFlow<AppNavigationState> = MutableStateFlow(
|
||||
override val appNavigationState: MutableStateFlow<AppNavigationState> = MutableStateFlow(
|
||||
AppNavigationState(
|
||||
navigationState = NavigationState.Root,
|
||||
isInForeground = true,
|
||||
)
|
||||
),
|
||||
) : AppNavigationStateService {
|
||||
|
||||
override val appNavigationState: StateFlow<AppNavigationState> = fakeAppNavigationState
|
||||
|
||||
override fun onNavigateToSession(owner: String, sessionId: SessionId) = Unit
|
||||
override fun onLeavingSession(owner: String) = Unit
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue