Use coroutine dispatcher instead of WorkerThread

This commit is contained in:
Benoit Marty 2023-05-24 17:58:16 +02:00 committed by Benoit Marty
parent 3d77083aa7
commit fe85a7cc92
8 changed files with 48 additions and 70 deletions

View file

@ -19,7 +19,6 @@ package io.element.android.libraries.push.impl.notifications
import android.content.Context import android.content.Context
import android.graphics.Bitmap import android.graphics.Bitmap
import android.os.Build import android.os.Build
import androidx.annotation.WorkerThread
import androidx.core.graphics.drawable.IconCompat import androidx.core.graphics.drawable.IconCompat
import androidx.core.graphics.drawable.toBitmap import androidx.core.graphics.drawable.toBitmap
import coil.imageLoader import coil.imageLoader
@ -27,7 +26,6 @@ import coil.request.ImageRequest
import coil.transform.CircleCropTransformation import coil.transform.CircleCropTransformation
import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.matrix.api.media.MediaResolver import io.element.android.libraries.matrix.api.media.MediaResolver
import kotlinx.coroutines.runBlocking
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -39,24 +37,20 @@ class NotificationBitmapLoader @Inject constructor(
* Get icon of a room. * Get icon of a room.
* @param path mxc url * @param path mxc url
*/ */
@WorkerThread suspend fun getRoomBitmap(path: String?): Bitmap? {
fun getRoomBitmap(path: String?): Bitmap? {
if (path == null) { if (path == null) {
return null return null
} }
return loadRoomBitmap(path) return loadRoomBitmap(path)
} }
@WorkerThread private suspend fun loadRoomBitmap(path: String): Bitmap? {
private fun loadRoomBitmap(path: String): Bitmap? {
return try { return try {
val imageRequest = ImageRequest.Builder(context) val imageRequest = ImageRequest.Builder(context)
.data(MediaResolver.Meta(path, MediaResolver.Kind.Thumbnail(1024))) .data(MediaResolver.Meta(path, MediaResolver.Kind.Thumbnail(1024)))
.build() .build()
runBlocking { val result = context.imageLoader.execute(imageRequest)
val result = context.imageLoader.execute(imageRequest) result.drawable?.toBitmap()
result.drawable?.toBitmap()
}
} catch (e: Throwable) { } catch (e: Throwable) {
Timber.e(e, "Unable to load room bitmap") Timber.e(e, "Unable to load room bitmap")
null null
@ -68,8 +62,7 @@ class NotificationBitmapLoader @Inject constructor(
* Before Android P, this does nothing because the icon won't be used * Before Android P, this does nothing because the icon won't be used
* @param path mxc url * @param path mxc url
*/ */
@WorkerThread suspend fun getUserIcon(path: String?): IconCompat? {
fun getUserIcon(path: String?): IconCompat? {
if (path == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.P) { if (path == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
return null return null
} }
@ -77,17 +70,14 @@ class NotificationBitmapLoader @Inject constructor(
return loadUserIcon(path) return loadUserIcon(path)
} }
@WorkerThread private suspend fun loadUserIcon(path: String): IconCompat? {
private fun loadUserIcon(path: String): IconCompat? {
return try { return try {
val imageRequest = ImageRequest.Builder(context) val imageRequest = ImageRequest.Builder(context)
.data(MediaResolver.Meta(path, MediaResolver.Kind.Thumbnail(1024))) .data(MediaResolver.Meta(path, MediaResolver.Kind.Thumbnail(1024)))
.transformations(CircleCropTransformation()) .transformations(CircleCropTransformation())
.build() .build()
val bitmap = runBlocking { val result = context.imageLoader.execute(imageRequest)
val result = context.imageLoader.execute(imageRequest) val bitmap = result.drawable?.toBitmap()
result.drawable?.toBitmap()
}
return bitmap?.let { IconCompat.createWithBitmap(it) } return bitmap?.let { IconCompat.createWithBitmap(it) }
} catch (e: Throwable) { } catch (e: Throwable) {
Timber.e(e, "Unable to load user bitmap") Timber.e(e, "Unable to load user bitmap")

View file

@ -16,11 +16,9 @@
package io.element.android.libraries.push.impl.notifications package io.element.android.libraries.push.impl.notifications
import android.os.Handler
import android.os.HandlerThread
import androidx.annotation.WorkerThread
import io.element.android.libraries.androidutils.throttler.FirstThrottler import io.element.android.libraries.androidutils.throttler.FirstThrottler
import io.element.android.libraries.core.cache.CircularCache import io.element.android.libraries.core.cache.CircularCache
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.data.tryOrNull import io.element.android.libraries.core.data.tryOrNull
import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.AppScope
@ -37,8 +35,9 @@ import io.element.android.libraries.push.impl.notifications.model.shouldIgnoreMe
import io.element.android.services.appnavstate.api.AppNavigationState import io.element.android.services.appnavstate.api.AppNavigationState
import io.element.android.services.appnavstate.api.AppNavigationStateService import io.element.android.services.appnavstate.api.AppNavigationStateService
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -56,13 +55,10 @@ class NotificationDrawerManager @Inject constructor(
private val filteredEventDetector: FilteredEventDetector, private val filteredEventDetector: FilteredEventDetector,
private val appNavigationStateService: AppNavigationStateService, private val appNavigationStateService: AppNavigationStateService,
private val coroutineScope: CoroutineScope, private val coroutineScope: CoroutineScope,
private val dispatchers: CoroutineDispatchers,
private val buildMeta: BuildMeta, private val buildMeta: BuildMeta,
private val matrixAuthenticationService: MatrixAuthenticationService, private val matrixAuthenticationService: MatrixAuthenticationService,
) { ) {
private val handlerThread: HandlerThread = HandlerThread("NotificationDrawerManager", Thread.MIN_PRIORITY)
private var backgroundHandler: Handler
/** /**
* Lazily initializes the NotificationState as we rely on having a current session in order to fetch the persisted queue of events. * Lazily initializes the NotificationState as we rely on having a current session in order to fetch the persisted queue of events.
*/ */
@ -74,8 +70,6 @@ class NotificationDrawerManager @Inject constructor(
private var useCompleteNotificationFormat = true private var useCompleteNotificationFormat = true
init { init {
handlerThread.start()
backgroundHandler = Handler(handlerThread.looper)
// Observe application state // Observe application state
coroutineScope.launch { coroutineScope.launch {
appNavigationStateService.appNavigationStateFlow appNavigationStateService.appNavigationStateFlow
@ -193,30 +187,25 @@ class NotificationDrawerManager @Inject constructor(
notificationState.updateQueuedEvents(this) { queuedEvents, _ -> notificationState.updateQueuedEvents(this) { queuedEvents, _ ->
action(queuedEvents) action(queuedEvents)
} }
refreshNotificationDrawer() coroutineScope.refreshNotificationDrawer()
} }
private fun refreshNotificationDrawer() { private fun CoroutineScope.refreshNotificationDrawer() = launch {
// Implement last throttler // Implement last throttler
val canHandle = firstThrottler.canHandle() val canHandle = firstThrottler.canHandle()
Timber.v("refreshNotificationDrawer(), delay: ${canHandle.waitMillis()} ms") Timber.v("refreshNotificationDrawer(), delay: ${canHandle.waitMillis()} ms")
backgroundHandler.removeCallbacksAndMessages(null) withContext(dispatchers.io) {
delay(canHandle.waitMillis())
backgroundHandler.postDelayed( try {
{ refreshNotificationDrawerBg()
try { } catch (throwable: Throwable) {
refreshNotificationDrawerBg() // It can happen if for instance session has been destroyed. It's a bit ugly to try catch like this, but it's safer
} catch (throwable: Throwable) { Timber.w(throwable, "refreshNotificationDrawerBg failure")
// It can happen if for instance session has been destroyed. It's a bit ugly to try catch like this, but it's safer }
Timber.w(throwable, "refreshNotificationDrawerBg failure") }
}
},
canHandle.waitMillis()
)
} }
@WorkerThread private suspend fun refreshNotificationDrawerBg() {
private fun refreshNotificationDrawerBg() {
Timber.v("refreshNotificationDrawerBg()") Timber.v("refreshNotificationDrawerBg()")
val eventsToRender = notificationState.updateQueuedEvents(this) { queuedEvents, renderedEvents -> val eventsToRender = notificationState.updateQueuedEvents(this) { queuedEvents, renderedEvents ->
notifiableEventProcessor.process(queuedEvents.rawEvents(), currentAppNavigationState, renderedEvents).also { notifiableEventProcessor.process(queuedEvents.rawEvents(), currentAppNavigationState, renderedEvents).also {
@ -239,8 +228,7 @@ class NotificationDrawerManager @Inject constructor(
} }
} }
@WorkerThread private suspend fun renderEvents(eventsToRender: List<ProcessedEvent<NotifiableEvent>>) {
private fun renderEvents(eventsToRender: List<ProcessedEvent<NotifiableEvent>>) {
// Group by sessionId // Group by sessionId
val eventsForSessions = eventsToRender.groupBy { val eventsForSessions = eventsToRender.groupBy {
it.event.sessionId it.event.sessionId
@ -250,18 +238,16 @@ class NotificationDrawerManager @Inject constructor(
val currentUser = tryOrNull( val currentUser = tryOrNull(
onError = { Timber.e(it, "Unable to retrieve info for user ${sessionId.value}") }, onError = { Timber.e(it, "Unable to retrieve info for user ${sessionId.value}") },
operation = { operation = {
runBlocking { val client = matrixAuthenticationService.restoreSession(sessionId).getOrNull()
val client = matrixAuthenticationService.restoreSession(sessionId).getOrNull()
// myUserDisplayName cannot be empty else NotificationCompat.MessagingStyle() will crash // myUserDisplayName cannot be empty else NotificationCompat.MessagingStyle() will crash
val myUserDisplayName = client?.loadUserDisplayName()?.getOrNull() ?: sessionId.value val myUserDisplayName = client?.loadUserDisplayName()?.getOrNull() ?: sessionId.value
val userAvatarUrl = client?.loadUserAvatarURLString()?.getOrNull() val userAvatarUrl = client?.loadUserAvatarURLString()?.getOrNull()
MatrixUser( MatrixUser(
userId = sessionId, userId = sessionId,
displayName = myUserDisplayName, displayName = myUserDisplayName,
avatarUrl = userAvatarUrl avatarUrl = userAvatarUrl
) )
}
} }
) ?: MatrixUser( ) ?: MatrixUser(
userId = sessionId, userId = sessionId,

View file

@ -34,7 +34,7 @@ class NotificationFactory @Inject constructor(
private val summaryGroupMessageCreator: SummaryGroupMessageCreator private val summaryGroupMessageCreator: SummaryGroupMessageCreator
) { ) {
fun Map<RoomId, ProcessedMessageEvents>.toNotifications( suspend fun Map<RoomId, ProcessedMessageEvents>.toNotifications(
currentUser: MatrixUser, currentUser: MatrixUser,
): List<RoomNotification> { ): List<RoomNotification> {
return map { (roomId, events) -> return map { (roomId, events) ->

View file

@ -16,7 +16,6 @@
package io.element.android.libraries.push.impl.notifications package io.element.android.libraries.push.impl.notifications
import androidx.annotation.WorkerThread
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent
@ -32,8 +31,7 @@ class NotificationRenderer @Inject constructor(
private val notificationFactory: NotificationFactory, private val notificationFactory: NotificationFactory,
) { ) {
@WorkerThread suspend fun render(
fun render(
currentUser: MatrixUser, currentUser: MatrixUser,
useCompleteNotificationFormat: Boolean, useCompleteNotificationFormat: Boolean,
eventsToProcess: List<ProcessedEvent<NotifiableEvent>> eventsToProcess: List<ProcessedEvent<NotifiableEvent>>

View file

@ -37,7 +37,7 @@ class RoomGroupMessageCreator @Inject constructor(
private val notificationFactory: NotificationFactory private val notificationFactory: NotificationFactory
) { ) {
fun createRoomMessage( suspend fun createRoomMessage(
currentUser: MatrixUser, currentUser: MatrixUser,
events: List<NotifiableMessageEvent>, events: List<NotifiableMessageEvent>,
roomId: RoomId, roomId: RoomId,
@ -98,7 +98,7 @@ class RoomGroupMessageCreator @Inject constructor(
) )
} }
private fun NotificationCompat.MessagingStyle.addMessagesFromEvents(events: List<NotifiableMessageEvent>) { private suspend fun NotificationCompat.MessagingStyle.addMessagesFromEvents(events: List<NotifiableMessageEvent>) {
events.forEach { event -> events.forEach { event ->
val senderPerson = if (event.outGoingMessage) { val senderPerson = if (event.outGoingMessage) {
null null
@ -171,7 +171,7 @@ class RoomGroupMessageCreator @Inject constructor(
} }
} }
private fun getRoomBitmap(events: List<NotifiableMessageEvent>): Bitmap? { private suspend fun getRoomBitmap(events: List<NotifiableMessageEvent>): Bitmap? {
// Use the last event (most recent?) // Use the last event (most recent?)
return events.lastOrNull() return events.lastOrNull()
?.roomAvatarPath ?.roomAvatarPath

View file

@ -28,6 +28,7 @@ import io.element.android.libraries.push.impl.notifications.fake.FakeSummaryGrou
import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent
import io.element.android.libraries.push.impl.notifications.fixtures.aSimpleNotifiableEvent import io.element.android.libraries.push.impl.notifications.fixtures.aSimpleNotifiableEvent
import io.element.android.libraries.push.impl.notifications.fixtures.anInviteNotifiableEvent import io.element.android.libraries.push.impl.notifications.fixtures.anInviteNotifiableEvent
import kotlinx.coroutines.test.runTest
import org.junit.Test import org.junit.Test
private val MY_AVATAR_URL: String? = null private val MY_AVATAR_URL: String? = null
@ -196,6 +197,8 @@ class NotificationFactoryTest {
} }
} }
fun <T> testWith(receiver: T, block: T.() -> Unit) { fun <T> testWith(receiver: T, block: suspend T.() -> Unit) {
receiver.block() runTest {
receiver.block()
}
} }

View file

@ -22,6 +22,7 @@ import io.element.android.libraries.push.impl.notifications.NotificationFactory
import io.element.android.libraries.push.impl.notifications.OneShotNotification import io.element.android.libraries.push.impl.notifications.OneShotNotification
import io.element.android.libraries.push.impl.notifications.RoomNotification import io.element.android.libraries.push.impl.notifications.RoomNotification
import io.element.android.libraries.push.impl.notifications.SummaryNotification import io.element.android.libraries.push.impl.notifications.SummaryNotification
import io.mockk.coEvery
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
@ -38,7 +39,7 @@ class FakeNotificationFactory {
summaryNotification: SummaryNotification summaryNotification: SummaryNotification
) { ) {
with(instance) { with(instance) {
every { groupedEvents.roomEvents.toNotifications(matrixUser) } returns roomNotifications coEvery { groupedEvents.roomEvents.toNotifications(matrixUser) } returns roomNotifications
every { groupedEvents.invitationEvents.toNotifications() } returns invitationNotifications every { groupedEvents.invitationEvents.toNotifications() } returns invitationNotifications
every { groupedEvents.simpleEvents.toNotifications() } returns simpleNotifications every { groupedEvents.simpleEvents.toNotifications() } returns simpleNotifications

View file

@ -21,7 +21,7 @@ import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.push.impl.notifications.RoomGroupMessageCreator import io.element.android.libraries.push.impl.notifications.RoomGroupMessageCreator
import io.element.android.libraries.push.impl.notifications.RoomNotification import io.element.android.libraries.push.impl.notifications.RoomNotification
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
import io.mockk.every import io.mockk.coEvery
import io.mockk.mockk import io.mockk.mockk
class FakeRoomGroupMessageCreator { class FakeRoomGroupMessageCreator {
@ -34,7 +34,7 @@ class FakeRoomGroupMessageCreator {
roomId: RoomId, roomId: RoomId,
): RoomNotification.Message { ): RoomNotification.Message {
val mockMessage = mockk<RoomNotification.Message>() val mockMessage = mockk<RoomNotification.Message>()
every { coEvery {
instance.createRoomMessage( instance.createRoomMessage(
currentUser = matrixUser, currentUser = matrixUser,
events = events, events = events,