Use coroutine dispatcher instead of WorkerThread
This commit is contained in:
parent
3d77083aa7
commit
fe85a7cc92
8 changed files with 48 additions and 70 deletions
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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) ->
|
||||||
|
|
|
||||||
|
|
@ -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>>
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue