diff --git a/app/src/main/kotlin/app/dapk/st/SmallTalkApplication.kt b/app/src/main/kotlin/app/dapk/st/SmallTalkApplication.kt index 1d15604..3fb4488 100644 --- a/app/src/main/kotlin/app/dapk/st/SmallTalkApplication.kt +++ b/app/src/main/kotlin/app/dapk/st/SmallTalkApplication.kt @@ -60,7 +60,7 @@ class SmallTalkApplication : Application(), ModuleProvider { applicationScope.launch { val notificationsUseCase = notificationsModule.notificationsUseCase() - notificationsUseCase.listenForNotificationChanges() + notificationsUseCase.listenForNotificationChanges(this) } } diff --git a/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt b/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt index e56cf2e..8b013f4 100644 --- a/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt +++ b/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt @@ -199,6 +199,7 @@ internal class FeatureModules internal constructor( NotificationsModule( imageLoaderModule.iconLoader(), storeModule.value.roomStore(), + storeModule.value.overviewStore(), context, intentFactory = coreAndroidModule.intentFactory(), dispatchers = coroutineDispatchers, diff --git a/features/notifications/src/main/kotlin/app/dapk/st/notifications/AndroidNotificationBuilder.kt b/features/notifications/src/main/kotlin/app/dapk/st/notifications/AndroidNotificationBuilder.kt index 6f49daa..5cfbd09 100644 --- a/features/notifications/src/main/kotlin/app/dapk/st/notifications/AndroidNotificationBuilder.kt +++ b/features/notifications/src/main/kotlin/app/dapk/st/notifications/AndroidNotificationBuilder.kt @@ -32,6 +32,8 @@ class AndroidNotificationBuilder( .apply { setGroupSummary(notification.isGroupSummary) } .ifNotNull(notification.groupId) { setGroup(it) } .ifNotNull(notification.messageStyle) { style = it.build(notificationStyleBuilder) } + .ifNotNull(notification.contentTitle) { setContentTitle(it) } + .ifNotNull(notification.contentText) { setContentText(it) } .ifNotNull(notification.contentIntent) { setContentIntent(it) } .ifNotNull(notification.whenTimestamp) { setShowWhen(true) @@ -65,6 +67,8 @@ data class AndroidNotification( val shortcutId: String? = null, val alertMoreThanOnce: Boolean, val contentIntent: PendingIntent? = null, + val contentTitle: String? = null, + val contentText: String? = null, val messageStyle: AndroidNotificationStyle? = null, val category: String? = null, val smallIcon: Int? = null, diff --git a/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationChannels.kt b/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationChannels.kt index 12dbd8d..0daa08c 100644 --- a/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationChannels.kt +++ b/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationChannels.kt @@ -8,6 +8,7 @@ import android.os.Build const val DIRECT_CHANNEL_ID = "direct_channel_id" const val GROUP_CHANNEL_ID = "group_channel_id" const val SUMMARY_CHANNEL_ID = "summary_channel_id" +const val INVITE_CHANNEL_ID = "invite_channel_id" private const val CHATS_NOTIFICATION_GROUP_ID = "chats_notification_group" @@ -45,6 +46,18 @@ class NotificationChannels( ) } + if (notificationManager.getNotificationChannel(INVITE_CHANNEL_ID) == null) { + notificationManager.createNotificationChannel( + NotificationChannel( + INVITE_CHANNEL_ID, + "Invite notifications", + NotificationManager.IMPORTANCE_DEFAULT, + ).also { + it.group = CHATS_NOTIFICATION_GROUP_ID + } + ) + } + if (notificationManager.getNotificationChannel(SUMMARY_CHANNEL_ID) == null) { notificationManager.createNotificationChannel( NotificationChannel( diff --git a/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationFactory.kt b/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationFactory.kt index 13d26a1..503e074 100644 --- a/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationFactory.kt +++ b/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationFactory.kt @@ -8,6 +8,7 @@ import app.dapk.st.imageloader.IconLoader import app.dapk.st.matrix.common.RoomId import app.dapk.st.matrix.sync.RoomOverview import app.dapk.st.navigator.IntentFactory +import java.time.Clock private const val GROUP_ID = "st" @@ -17,6 +18,7 @@ class NotificationFactory( private val intentFactory: IntentFactory, private val iconLoader: IconLoader, private val deviceMeta: DeviceMeta, + private val clock: Clock, ) { private val shouldAlwaysAlertDms = true @@ -84,6 +86,21 @@ class NotificationFactory( category = Notification.CATEGORY_MESSAGE, ) } + + fun createInvite(inviteNotification: InviteNotification): AndroidNotification { + val openAppIntent = intentFactory.notificationOpenApp(context) + return AndroidNotification( + channelId = INVITE_CHANNEL_ID, + smallIcon = R.drawable.ic_notification_small_icon, + whenTimestamp = clock.millis(), + alertMoreThanOnce = true, + contentTitle = "Invite", + contentText = inviteNotification.content, + contentIntent = openAppIntent, + category = Notification.CATEGORY_EVENT, + autoCancel = true, + ) + } } private fun List.mostRecent() = this.sortedBy { it.notification.whenTimestamp }.first() diff --git a/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationInviteRenderer.kt b/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationInviteRenderer.kt new file mode 100644 index 0000000..8987ea9 --- /dev/null +++ b/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationInviteRenderer.kt @@ -0,0 +1,25 @@ +package app.dapk.st.notifications + +import android.app.NotificationManager + +private const val INVITE_NOTIFICATION_ID = 103 + +class NotificationInviteRenderer( + private val notificationManager: NotificationManager, + private val notificationFactory: NotificationFactory, + private val androidNotificationBuilder: AndroidNotificationBuilder, +) { + + fun render(inviteNotification: InviteNotification) { + notificationManager.notify( + inviteNotification.roomId.value, + INVITE_NOTIFICATION_ID, + inviteNotification.toAndroidNotification() + ) + } + + private fun InviteNotification.toAndroidNotification() = androidNotificationBuilder.build( + notificationFactory.createInvite(this) + ) + +} \ No newline at end of file diff --git a/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationRenderer.kt b/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationMessageRenderer.kt similarity index 98% rename from features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationRenderer.kt rename to features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationMessageRenderer.kt index 5a2cc7c..1a12ddb 100644 --- a/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationRenderer.kt +++ b/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationMessageRenderer.kt @@ -13,7 +13,7 @@ import kotlinx.coroutines.withContext private const val SUMMARY_NOTIFICATION_ID = 101 private const val MESSAGE_NOTIFICATION_ID = 100 -class NotificationRenderer( +class NotificationMessageRenderer( private val notificationManager: NotificationManager, private val notificationStateMapper: NotificationStateMapper, private val androidNotificationBuilder: AndroidNotificationBuilder, diff --git a/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationsModule.kt b/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationsModule.kt index f3501b9..3368110 100644 --- a/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationsModule.kt +++ b/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationsModule.kt @@ -6,37 +6,46 @@ import app.dapk.st.core.CoroutineDispatchers import app.dapk.st.core.DeviceMeta import app.dapk.st.core.ProvidableModule import app.dapk.st.imageloader.IconLoader +import app.dapk.st.matrix.sync.OverviewStore import app.dapk.st.matrix.sync.RoomStore import app.dapk.st.navigator.IntentFactory +import java.time.Clock class NotificationsModule( private val iconLoader: IconLoader, private val roomStore: RoomStore, + private val overviewStore: OverviewStore, private val context: Context, private val intentFactory: IntentFactory, private val dispatchers: CoroutineDispatchers, private val deviceMeta: DeviceMeta, ) : ProvidableModule { - fun notificationsUseCase() = RenderNotificationsUseCase( - notificationRenderer = NotificationRenderer( - notificationManager(), - NotificationStateMapper( - RoomEventsToNotifiableMapper(), - NotificationFactory( - context, - NotificationStyleFactory(iconLoader, deviceMeta), - intentFactory, - iconLoader, - deviceMeta, - ) - ), - AndroidNotificationBuilder(context, deviceMeta, AndroidNotificationStyleBuilder()), + fun notificationsUseCase(): RenderNotificationsUseCase { + val notificationManager = notificationManager() + val androidNotificationBuilder = AndroidNotificationBuilder(context, deviceMeta, AndroidNotificationStyleBuilder()) + val notificationFactory = NotificationFactory( + context, + NotificationStyleFactory(iconLoader, deviceMeta), + intentFactory, + iconLoader, + deviceMeta, + Clock.systemUTC(), + ) + val notificationMessageRenderer = NotificationMessageRenderer( + notificationManager, + NotificationStateMapper(RoomEventsToNotifiableMapper(), notificationFactory), + androidNotificationBuilder, dispatchers - ), - observeRenderableUnreadEventsUseCase = ObserveUnreadNotificationsUseCaseImpl(roomStore), - notificationChannels = NotificationChannels(notificationManager()), - ) + ) + return RenderNotificationsUseCase( + notificationRenderer = notificationMessageRenderer, + observeRenderableUnreadEventsUseCase = ObserveUnreadNotificationsUseCaseImpl(roomStore), + notificationChannels = NotificationChannels(notificationManager), + observeInviteNotificationsUseCase = ObserveInviteNotificationsUseCaseImpl(overviewStore), + inviteRenderer = NotificationInviteRenderer(notificationManager, notificationFactory, androidNotificationBuilder) + ) + } private fun notificationManager() = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager diff --git a/features/notifications/src/main/kotlin/app/dapk/st/notifications/ObserveInviteNotificationsUseCase.kt b/features/notifications/src/main/kotlin/app/dapk/st/notifications/ObserveInviteNotificationsUseCase.kt new file mode 100644 index 0000000..802fb24 --- /dev/null +++ b/features/notifications/src/main/kotlin/app/dapk/st/notifications/ObserveInviteNotificationsUseCase.kt @@ -0,0 +1,49 @@ +package app.dapk.st.notifications + +import app.dapk.st.matrix.common.RoomId +import app.dapk.st.matrix.sync.InviteMeta +import app.dapk.st.matrix.sync.OverviewStore +import app.dapk.st.matrix.sync.RoomInvite +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.flow.* + +internal typealias ObserveInviteNotificationsUseCase = suspend () -> Flow + +class ObserveInviteNotificationsUseCaseImpl(private val overviewStore: OverviewStore) : ObserveInviteNotificationsUseCase { + + override suspend fun invoke(): Flow { + return overviewStore.latestInvites() + .diff() + .flatten() + .map { + val text = when (val meta = it.inviteMeta) { + InviteMeta.DirectMessage -> "${it.inviterName()} has invited you to chat" + is InviteMeta.Room -> "${it.inviterName()} has invited you to ${meta.roomName ?: "unnamed room"}" + } + InviteNotification(content = text, roomId = it.roomId) + } + } + + private fun Flow>.diff(): Flow> { + val previousInvites = mutableSetOf() + return this.distinctUntilChanged() + .map { + val diff = it.toSet() - previousInvites + previousInvites.clear() + previousInvites.addAll(it) + diff + } + } + + private fun RoomInvite.inviterName() = this.from.displayName?.let { "$it (${this.from.id.value})" } ?: this.from.id.value +} + +@OptIn(FlowPreview::class) +private fun Flow>.flatten() = this.flatMapConcat { items -> + flow { items.forEach { this.emit(it) } } +} + +data class InviteNotification( + val content: String, + val roomId: RoomId +) \ No newline at end of file diff --git a/features/notifications/src/main/kotlin/app/dapk/st/notifications/RenderNotificationsUseCase.kt b/features/notifications/src/main/kotlin/app/dapk/st/notifications/RenderNotificationsUseCase.kt index 59128eb..d51f6e8 100644 --- a/features/notifications/src/main/kotlin/app/dapk/st/notifications/RenderNotificationsUseCase.kt +++ b/features/notifications/src/main/kotlin/app/dapk/st/notifications/RenderNotificationsUseCase.kt @@ -2,21 +2,27 @@ package app.dapk.st.notifications import app.dapk.st.matrix.sync.RoomEvent import app.dapk.st.matrix.sync.RoomOverview -import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.onStart class RenderNotificationsUseCase( - private val notificationRenderer: NotificationRenderer, + private val notificationRenderer: NotificationMessageRenderer, + private val inviteRenderer: NotificationInviteRenderer, private val observeRenderableUnreadEventsUseCase: ObserveUnreadNotificationsUseCase, + private val observeInviteNotificationsUseCase: ObserveInviteNotificationsUseCase, private val notificationChannels: NotificationChannels, ) { - suspend fun listenForNotificationChanges() { + suspend fun listenForNotificationChanges(scope: CoroutineScope) { + notificationChannels.initChannels() observeRenderableUnreadEventsUseCase() - .onStart { notificationChannels.initChannels() } .onEach { (each, diff) -> renderUnreadChange(each, diff) } - .collect() + .launchIn(scope) + + observeInviteNotificationsUseCase() + .onEach { inviteRenderer.render(it) } + .launchIn(scope) } private suspend fun renderUnreadChange(allUnread: Map>, diff: NotificationDiff) { diff --git a/features/notifications/src/test/kotlin/app/dapk/st/notifications/NotificationFactoryTest.kt b/features/notifications/src/test/kotlin/app/dapk/st/notifications/NotificationFactoryTest.kt index 60d5e0e..014cce6 100644 --- a/features/notifications/src/test/kotlin/app/dapk/st/notifications/NotificationFactoryTest.kt +++ b/features/notifications/src/test/kotlin/app/dapk/st/notifications/NotificationFactoryTest.kt @@ -16,6 +16,9 @@ import io.mockk.mockk import kotlinx.coroutines.test.runTest import org.amshove.kluent.shouldBeEqualTo import org.junit.Test +import java.time.Clock +import java.time.Instant +import java.time.ZoneOffset private const val A_CHANNEL_ID = "a channel id" private val AN_OPEN_APP_INTENT = aPendingIntent() @@ -38,6 +41,7 @@ class NotificationFactoryTest { private val fakeNotificationStyleFactory = FakeNotificationStyleFactory() private val fakeIntentFactory = FakeIntentFactory() private val fakeIconLoader = FakeIconLoader() + private val fixedClock = Clock.fixed(Instant.ofEpochMilli(0), ZoneOffset.UTC) private val notificationFactory = NotificationFactory( fakeContext.instance, @@ -45,6 +49,7 @@ class NotificationFactoryTest { fakeIntentFactory, fakeIconLoader, DeviceMeta(26), + fixedClock ) @Test @@ -127,6 +132,30 @@ class NotificationFactoryTest { ) } + @Test + fun `given invite, then creates expected`() { + fakeIntentFactory.givenNotificationOpenApp(fakeContext.instance).returns(AN_OPEN_APP_INTENT) + val content = "Content message" + val result = notificationFactory.createInvite( + InviteNotification( + content = content, + A_ROOM_ID, + ) + ) + + result shouldBeEqualTo AndroidNotification( + channelId = INVITE_CHANNEL_ID, + whenTimestamp = fixedClock.millis(), + alertMoreThanOnce = true, + smallIcon = R.drawable.ic_notification_small_icon, + contentIntent = AN_OPEN_APP_INTENT, + category = Notification.CATEGORY_EVENT, + autoCancel = true, + contentTitle = "Invite", + contentText = content, + ) + } + private fun givenEventsFor(roomOverview: RoomOverview) { fakeIntentFactory.givenNotificationOpenMessage(fakeContext.instance, roomOverview.roomId).returns(AN_OPEN_ROOM_INTENT) fakeNotificationStyleFactory.givenMessage(EVENTS.sortedBy { it.utcTimestamp }, roomOverview).returns(A_NOTIFICATION_STYLE) diff --git a/features/notifications/src/test/kotlin/app/dapk/st/notifications/NotificationRendererTest.kt b/features/notifications/src/test/kotlin/app/dapk/st/notifications/NotificationRendererTest.kt index ed128f2..0be9e2f 100644 --- a/features/notifications/src/test/kotlin/app/dapk/st/notifications/NotificationRendererTest.kt +++ b/features/notifications/src/test/kotlin/app/dapk/st/notifications/NotificationRendererTest.kt @@ -35,7 +35,7 @@ class NotificationRendererTest { private val fakeNotificationFactory = FakeNotificationFactory() private val fakeAndroidNotificationBuilder = FakeAndroidNotificationBuilder() - private val notificationRenderer = NotificationRenderer( + private val notificationRenderer = NotificationMessageRenderer( fakeNotificationManager.instance, fakeNotificationFactory.instance, fakeAndroidNotificationBuilder.instance, diff --git a/features/notifications/src/test/kotlin/app/dapk/st/notifications/RenderNotificationsUseCaseTest.kt b/features/notifications/src/test/kotlin/app/dapk/st/notifications/RenderNotificationsUseCaseTest.kt index 6cc7836..5175956 100644 --- a/features/notifications/src/test/kotlin/app/dapk/st/notifications/RenderNotificationsUseCaseTest.kt +++ b/features/notifications/src/test/kotlin/app/dapk/st/notifications/RenderNotificationsUseCaseTest.kt @@ -1,9 +1,9 @@ package app.dapk.st.notifications -import fake.FakeNotificationChannels -import fake.FakeNotificationRenderer -import fake.FakeObserveUnreadNotificationsUseCase +import fake.* import fixture.NotificationDiffFixtures.aNotificationDiff +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest import org.junit.Test import test.expect @@ -12,35 +12,41 @@ private val AN_UNREAD_NOTIFICATIONS = UnreadNotifications(emptyMap(), aNotificat class RenderNotificationsUseCaseTest { - private val fakeNotificationRenderer = FakeNotificationRenderer() + private val fakeNotificationMessageRenderer = FakeNotificationMessageRenderer() + private val fakeNotificationInviteRenderer = FakeNotificationInviteRenderer() private val fakeObserveUnreadNotificationsUseCase = FakeObserveUnreadNotificationsUseCase() + private val fakeObserveInviteNotificationsUseCase = FakeObserveInviteNotificationsUseCase() private val fakeNotificationChannels = FakeNotificationChannels().also { it.instance.expect { it.initChannels() } } private val renderNotificationsUseCase = RenderNotificationsUseCase( - fakeNotificationRenderer.instance, + fakeNotificationMessageRenderer.instance, + fakeNotificationInviteRenderer.instance, fakeObserveUnreadNotificationsUseCase, + fakeObserveInviteNotificationsUseCase, fakeNotificationChannels.instance, ) @Test fun `given events, when listening for changes then initiates channels once`() = runTest { - fakeNotificationRenderer.instance.expect { it.render(any()) } + fakeNotificationMessageRenderer.instance.expect { it.render(any()) } fakeObserveUnreadNotificationsUseCase.given().emits(AN_UNREAD_NOTIFICATIONS) + fakeObserveInviteNotificationsUseCase.given().emits() - renderNotificationsUseCase.listenForNotificationChanges() + renderNotificationsUseCase.listenForNotificationChanges(TestScope(UnconfinedTestDispatcher())) fakeNotificationChannels.verifyInitiated() } @Test fun `given renderable unread events, when listening for changes, then renders change`() = runTest { - fakeNotificationRenderer.instance.expect { it.render(any()) } + fakeNotificationMessageRenderer.instance.expect { it.render(any()) } fakeObserveUnreadNotificationsUseCase.given().emits(AN_UNREAD_NOTIFICATIONS) + fakeObserveInviteNotificationsUseCase.given().emits() - renderNotificationsUseCase.listenForNotificationChanges() + renderNotificationsUseCase.listenForNotificationChanges(TestScope(UnconfinedTestDispatcher())) - fakeNotificationRenderer.verifyRenders(AN_UNREAD_NOTIFICATIONS) + fakeNotificationMessageRenderer.verifyRenders(AN_UNREAD_NOTIFICATIONS) } } diff --git a/features/notifications/src/test/kotlin/fake/FakeNotificationInviteRenderer.kt b/features/notifications/src/test/kotlin/fake/FakeNotificationInviteRenderer.kt new file mode 100644 index 0000000..aa9c662 --- /dev/null +++ b/features/notifications/src/test/kotlin/fake/FakeNotificationInviteRenderer.kt @@ -0,0 +1,8 @@ +package fake + +import app.dapk.st.notifications.NotificationInviteRenderer +import io.mockk.mockk + +class FakeNotificationInviteRenderer { + val instance = mockk() +} \ No newline at end of file diff --git a/features/notifications/src/test/kotlin/fake/FakeNotificationRenderer.kt b/features/notifications/src/test/kotlin/fake/FakeNotificationMessageRenderer.kt similarity index 81% rename from features/notifications/src/test/kotlin/fake/FakeNotificationRenderer.kt rename to features/notifications/src/test/kotlin/fake/FakeNotificationMessageRenderer.kt index fcdc118..288d694 100644 --- a/features/notifications/src/test/kotlin/fake/FakeNotificationRenderer.kt +++ b/features/notifications/src/test/kotlin/fake/FakeNotificationMessageRenderer.kt @@ -1,13 +1,13 @@ package fake -import app.dapk.st.notifications.NotificationRenderer +import app.dapk.st.notifications.NotificationMessageRenderer import app.dapk.st.notifications.NotificationState import app.dapk.st.notifications.UnreadNotifications import io.mockk.coVerify import io.mockk.mockk -class FakeNotificationRenderer { - val instance = mockk() +class FakeNotificationMessageRenderer { + val instance = mockk() fun verifyRenders(vararg unreadNotifications: UnreadNotifications) { unreadNotifications.forEach { unread -> @@ -23,4 +23,4 @@ class FakeNotificationRenderer { } } } -} \ No newline at end of file +} diff --git a/features/notifications/src/test/kotlin/fake/FakeObserveInviteNotificationsUseCase.kt b/features/notifications/src/test/kotlin/fake/FakeObserveInviteNotificationsUseCase.kt new file mode 100644 index 0000000..fba079f --- /dev/null +++ b/features/notifications/src/test/kotlin/fake/FakeObserveInviteNotificationsUseCase.kt @@ -0,0 +1,10 @@ +package fake + +import app.dapk.st.notifications.ObserveInviteNotificationsUseCase +import io.mockk.coEvery +import io.mockk.mockk +import test.delegateEmit + +class FakeObserveInviteNotificationsUseCase : ObserveInviteNotificationsUseCase by mockk() { + fun given() = coEvery { this@FakeObserveInviteNotificationsUseCase.invoke() }.delegateEmit() +} \ No newline at end of file diff --git a/test-harness/src/test/kotlin/test/TestMatrix.kt b/test-harness/src/test/kotlin/test/TestMatrix.kt index b758207..590056e 100644 --- a/test-harness/src/test/kotlin/test/TestMatrix.kt +++ b/test-harness/src/test/kotlin/test/TestMatrix.kt @@ -164,7 +164,8 @@ class TestMatrix( ) } } - } + }, + roomInviteRemover = { storeModule.overviewStore().removeInvites(listOf(it)) } ) installSyncService( diff --git a/tools/coverage.gradle b/tools/coverage.gradle index 009e835..c4685d5 100644 --- a/tools/coverage.gradle +++ b/tools/coverage.gradle @@ -74,3 +74,12 @@ task allCodeCoverageReport(type: JacocoReport) { dependsOn { ["app:assembleDebug"] + projects*.test } initializeReport(it, projects, excludes) } + +task unitTestCodeCoverageReport(type: JacocoReport) { + outputs.upToDateWhen { false } + rootProject.apply plugin: 'jacoco' + def projects = collectProjects { !it.name.contains("test-harness") && !it.name.contains("stub") && !it.name.contains("-noop") } + dependsOn { ["app:assembleDebug"] + projects*.test } + initializeReport(it, projects, excludes) +} +