Merge pull request #1992 from vector-im/feature/bma/notificationUserAvatar
Render images in notification
This commit is contained in:
commit
9250ed4ef7
16 changed files with 336 additions and 37 deletions
1
changelog.d/1991.misc
Normal file
1
changelog.d/1991.misc
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Render images in Notification
|
||||||
|
|
@ -24,6 +24,7 @@ import io.element.android.libraries.di.CacheDirectory
|
||||||
import io.element.android.libraries.di.RoomScope
|
import io.element.android.libraries.di.RoomScope
|
||||||
import io.element.android.libraries.matrix.api.media.MatrixMediaLoader
|
import io.element.android.libraries.matrix.api.media.MatrixMediaLoader
|
||||||
import io.element.android.libraries.matrix.api.media.MediaSource
|
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||||
|
import io.element.android.libraries.matrix.api.mxc.MxcTools
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -66,6 +67,7 @@ interface VoiceMessageMediaRepo {
|
||||||
|
|
||||||
class DefaultVoiceMessageMediaRepo @AssistedInject constructor(
|
class DefaultVoiceMessageMediaRepo @AssistedInject constructor(
|
||||||
@CacheDirectory private val cacheDir: File,
|
@CacheDirectory private val cacheDir: File,
|
||||||
|
mxcTools: MxcTools,
|
||||||
private val matrixMediaLoader: MatrixMediaLoader,
|
private val matrixMediaLoader: MatrixMediaLoader,
|
||||||
@Assisted private val mediaSource: MediaSource,
|
@Assisted private val mediaSource: MediaSource,
|
||||||
@Assisted("mimeType") private val mimeType: String?,
|
@Assisted("mimeType") private val mimeType: String?,
|
||||||
|
|
@ -101,7 +103,7 @@ class DefaultVoiceMessageMediaRepo @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val cachedFile: File? = mxcUri2FilePath(mediaSource.url)?.let {
|
private val cachedFile: File? = mxcTools.mxcUri2FilePath(mediaSource.url)?.let {
|
||||||
File("${cacheDir.path}/$CACHE_VOICE_SUBDIR/$it")
|
File("${cacheDir.path}/$CACHE_VOICE_SUBDIR/$it")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -110,24 +112,3 @@ class DefaultVoiceMessageMediaRepo @AssistedInject constructor(
|
||||||
* Subdirectory of the application's cache directory where voice messages are stored.
|
* Subdirectory of the application's cache directory where voice messages are stored.
|
||||||
*/
|
*/
|
||||||
private const val CACHE_VOICE_SUBDIR = "temp/voice"
|
private const val CACHE_VOICE_SUBDIR = "temp/voice"
|
||||||
|
|
||||||
/**
|
|
||||||
* Regex to match a Matrix Content (mxc://) URI.
|
|
||||||
*
|
|
||||||
* See: https://spec.matrix.org/v1.8/client-server-api/#matrix-content-mxc-uris
|
|
||||||
*/
|
|
||||||
private val mxcRegex = Regex("""^mxc:\/\/([^\/]+)\/([^\/]+)$""")
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sanitizes an mxcUri to be used as a relative file path.
|
|
||||||
*
|
|
||||||
* @param mxcUri the Matrix Content (mxc://) URI of the voice message.
|
|
||||||
* @return the relative file path as "<server-name>/<media-id>" or null if the mxcUri is invalid.
|
|
||||||
*/
|
|
||||||
private fun mxcUri2FilePath(mxcUri: String): String? = mxcRegex.matchEntire(mxcUri)?.let { match ->
|
|
||||||
buildString {
|
|
||||||
append(match.groupValues[1])
|
|
||||||
append("/")
|
|
||||||
append(match.groupValues[2])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.libraries.core.mimetype.MimeTypes
|
import io.element.android.libraries.core.mimetype.MimeTypes
|
||||||
import io.element.android.libraries.matrix.api.media.MatrixMediaLoader
|
import io.element.android.libraries.matrix.api.media.MatrixMediaLoader
|
||||||
import io.element.android.libraries.matrix.api.media.MediaSource
|
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||||
|
import io.element.android.libraries.matrix.api.mxc.MxcTools
|
||||||
import io.element.android.libraries.matrix.test.media.FakeMediaLoader
|
import io.element.android.libraries.matrix.test.media.FakeMediaLoader
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
|
|
@ -139,6 +140,7 @@ private fun createDefaultVoiceMessageMediaRepo(
|
||||||
mxcUri: String = MXC_URI,
|
mxcUri: String = MXC_URI,
|
||||||
) = DefaultVoiceMessageMediaRepo(
|
) = DefaultVoiceMessageMediaRepo(
|
||||||
cacheDir = temporaryFolder.root,
|
cacheDir = temporaryFolder.root,
|
||||||
|
mxcTools = MxcTools(),
|
||||||
matrixMediaLoader = matrixMediaLoader,
|
matrixMediaLoader = matrixMediaLoader,
|
||||||
mediaSource = MediaSource(
|
mediaSource = MediaSource(
|
||||||
url = mxcUri,
|
url = mxcUri,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
/*
|
||||||
|
* 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.matrix.api.mxc
|
||||||
|
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class MxcTools @Inject constructor() {
|
||||||
|
/**
|
||||||
|
* Regex to match a Matrix Content (mxc://) URI.
|
||||||
|
*
|
||||||
|
* See: https://spec.matrix.org/v1.8/client-server-api/#matrix-content-mxc-uris
|
||||||
|
*/
|
||||||
|
private val mxcRegex = Regex("""^mxc://([^/]+)/([^/]+)$""")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sanitizes an mxcUri to be used as a relative file path.
|
||||||
|
*
|
||||||
|
* @param mxcUri the Matrix Content (mxc://) URI of the file.
|
||||||
|
* @return the relative file path as "<server-name>/<media-id>" or null if the mxcUri is invalid.
|
||||||
|
*/
|
||||||
|
fun mxcUri2FilePath(mxcUri: String): String? = mxcRegex.matchEntire(mxcUri)?.let { match ->
|
||||||
|
buildString {
|
||||||
|
append(match.groupValues[1])
|
||||||
|
append("/")
|
||||||
|
append(match.groupValues[2])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -25,6 +25,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.MessageType
|
||||||
data class NotificationData(
|
data class NotificationData(
|
||||||
val eventId: EventId,
|
val eventId: EventId,
|
||||||
val roomId: RoomId,
|
val roomId: RoomId,
|
||||||
|
// mxc url
|
||||||
val senderAvatarUrl: String?,
|
val senderAvatarUrl: String?,
|
||||||
val senderDisplayName: String?,
|
val senderDisplayName: String?,
|
||||||
val roomAvatarUrl: String?,
|
val roomAvatarUrl: String?,
|
||||||
|
|
@ -34,8 +35,6 @@ data class NotificationData(
|
||||||
val isNoisy: Boolean,
|
val isNoisy: Boolean,
|
||||||
val timestamp: Long,
|
val timestamp: Long,
|
||||||
val content: NotificationContent,
|
val content: NotificationContent,
|
||||||
// For images for instance
|
|
||||||
val contentUrl: String?,
|
|
||||||
val hasMention: Boolean,
|
val hasMention: Boolean,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
/*
|
||||||
|
* 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.matrix.api.mxc
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class MxcToolsTest {
|
||||||
|
@Test
|
||||||
|
fun `mxcUri2FilePath returns extracted path`() {
|
||||||
|
val mxcTools = MxcTools()
|
||||||
|
val mxcUri = "mxc://server.org/abc123"
|
||||||
|
val filePath = mxcTools.mxcUri2FilePath(mxcUri)
|
||||||
|
assertThat(filePath).isEqualTo("server.org/abc123")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `mxcUri2FilePath returns null for invalid data`() {
|
||||||
|
val mxcTools = MxcTools()
|
||||||
|
assertThat(mxcTools.mxcUri2FilePath("")).isNull()
|
||||||
|
assertThat(mxcTools.mxcUri2FilePath("mxc://server.org")).isNull()
|
||||||
|
assertThat(mxcTools.mxcUri2FilePath("mxc://server.org/")).isNull()
|
||||||
|
assertThat(mxcTools.mxcUri2FilePath("m://server.org/abc123")).isNull()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -52,7 +52,6 @@ class NotificationMapper(
|
||||||
isNoisy = item.isNoisy.orFalse(),
|
isNoisy = item.isNoisy.orFalse(),
|
||||||
timestamp = item.timestamp() ?: clock.epochMillis(),
|
timestamp = item.timestamp() ?: clock.epochMillis(),
|
||||||
content = item.event.use { notificationContentMapper.map(it) },
|
content = item.event.use { notificationContentMapper.map(it) },
|
||||||
contentUrl = null,
|
|
||||||
hasMention = item.hasMention.orFalse(),
|
hasMention = item.hasMention.orFalse(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
<application>
|
<application>
|
||||||
<receiver
|
<receiver
|
||||||
|
|
@ -24,5 +25,17 @@
|
||||||
android:name=".notifications.NotificationBroadcastReceiver"
|
android:name=".notifications.NotificationBroadcastReceiver"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
|
<provider
|
||||||
|
android:name=".notifications.NotificationsFileProvider"
|
||||||
|
android:authorities="${applicationId}.notifications.fileprovider"
|
||||||
|
android:exported="false"
|
||||||
|
android:grantUriPermissions="true">
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
|
android:resource="@xml/notifications_provider_paths" />
|
||||||
|
</provider>
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,12 @@
|
||||||
|
|
||||||
package io.element.android.libraries.push.impl.notifications
|
package io.element.android.libraries.push.impl.notifications
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.core.content.FileProvider
|
||||||
import io.element.android.libraries.core.log.logger.LoggerTag
|
import io.element.android.libraries.core.log.logger.LoggerTag
|
||||||
|
import io.element.android.libraries.di.ApplicationContext
|
||||||
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import io.element.android.libraries.matrix.api.MatrixClientProvider
|
import io.element.android.libraries.matrix.api.MatrixClientProvider
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
|
@ -60,6 +65,8 @@ class NotifiableEventResolver @Inject constructor(
|
||||||
private val stringProvider: StringProvider,
|
private val stringProvider: StringProvider,
|
||||||
private val clock: SystemClock,
|
private val clock: SystemClock,
|
||||||
private val matrixClientProvider: MatrixClientProvider,
|
private val matrixClientProvider: MatrixClientProvider,
|
||||||
|
private val notificationMediaRepoFactory: NotificationMediaRepo.Factory,
|
||||||
|
@ApplicationContext private val context: Context,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
suspend fun resolveEvent(sessionId: SessionId, roomId: RoomId, eventId: EventId): NotifiableEvent? {
|
suspend fun resolveEvent(sessionId: SessionId, roomId: RoomId, eventId: EventId): NotifiableEvent? {
|
||||||
|
|
@ -75,10 +82,13 @@ class NotifiableEventResolver @Inject constructor(
|
||||||
}.getOrNull()
|
}.getOrNull()
|
||||||
|
|
||||||
// TODO this notificationData is not always valid at the moment, sometimes the Rust SDK can't fetch the matching event
|
// TODO this notificationData is not always valid at the moment, sometimes the Rust SDK can't fetch the matching event
|
||||||
return notificationData?.asNotifiableEvent(sessionId)
|
return notificationData?.asNotifiableEvent(client, sessionId)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun NotificationData.asNotifiableEvent(userId: SessionId): NotifiableEvent? {
|
private suspend fun NotificationData.asNotifiableEvent(
|
||||||
|
client: MatrixClient,
|
||||||
|
userId: SessionId,
|
||||||
|
): NotifiableEvent? {
|
||||||
return when (val content = this.content) {
|
return when (val content = this.content) {
|
||||||
is NotificationContent.MessageLike.RoomMessage -> {
|
is NotificationContent.MessageLike.RoomMessage -> {
|
||||||
val messageBody = descriptionFromMessageContent(content, senderDisplayName ?: content.senderId.value)
|
val messageBody = descriptionFromMessageContent(content, senderDisplayName ?: content.senderId.value)
|
||||||
|
|
@ -96,7 +106,7 @@ class NotifiableEventResolver @Inject constructor(
|
||||||
timestamp = this.timestamp,
|
timestamp = this.timestamp,
|
||||||
senderName = senderDisplayName,
|
senderName = senderDisplayName,
|
||||||
body = notificationBody,
|
body = notificationBody,
|
||||||
imageUriString = this.contentUrl,
|
imageUriString = fetchImageIfPresent(client)?.toString(),
|
||||||
roomName = roomDisplayName,
|
roomName = roomDisplayName,
|
||||||
roomIsDirect = isDirect,
|
roomIsDirect = isDirect,
|
||||||
roomAvatarPath = roomAvatarUrl,
|
roomAvatarPath = roomAvatarUrl,
|
||||||
|
|
@ -238,6 +248,34 @@ class NotifiableEventResolver @Inject constructor(
|
||||||
stringProvider.getString(R.string.notification_room_invite_body)
|
stringProvider.getString(R.string.notification_room_invite_body)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun NotificationData.fetchImageIfPresent(client: MatrixClient): Uri? {
|
||||||
|
val fileResult = when (val content = this.content) {
|
||||||
|
is NotificationContent.MessageLike.RoomMessage -> {
|
||||||
|
when (val messageType = content.messageType) {
|
||||||
|
is ImageMessageType -> notificationMediaRepoFactory.create(client)
|
||||||
|
.getMediaFile(
|
||||||
|
mediaSource = messageType.source,
|
||||||
|
mimeType = messageType.info?.mimetype,
|
||||||
|
body = messageType.body,
|
||||||
|
)
|
||||||
|
is VideoMessageType -> null // Use the thumbnail here?
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> null
|
||||||
|
} ?: return null
|
||||||
|
|
||||||
|
return fileResult
|
||||||
|
.onFailure {
|
||||||
|
Timber.tag(loggerTag.value).e(it, "Failed to download image for notification")
|
||||||
|
}
|
||||||
|
.map { mediaFile ->
|
||||||
|
val authority = "${context.packageName}.notifications.fileprovider"
|
||||||
|
FileProvider.getUriForFile(context, authority, mediaFile)
|
||||||
|
}
|
||||||
|
.getOrNull()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("LongParameterList")
|
@Suppress("LongParameterList")
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,120 @@
|
||||||
|
/*
|
||||||
|
* 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.squareup.anvil.annotations.ContributesBinding
|
||||||
|
import dagger.assisted.Assisted
|
||||||
|
import dagger.assisted.AssistedFactory
|
||||||
|
import dagger.assisted.AssistedInject
|
||||||
|
import io.element.android.libraries.di.AppScope
|
||||||
|
import io.element.android.libraries.di.CacheDirectory
|
||||||
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
|
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||||
|
import io.element.android.libraries.matrix.api.mxc.MxcTools
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the media file for a notification.
|
||||||
|
*
|
||||||
|
* Media is downloaded from the rust sdk and stored in the application's cache directory.
|
||||||
|
* Media files are indexed by their Matrix Content (mxc://) URI and considered immutable.
|
||||||
|
* Whenever a given mxc is found in the cache, it is returned immediately.
|
||||||
|
*/
|
||||||
|
interface NotificationMediaRepo {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Factory for [NotificationMediaRepo].
|
||||||
|
*/
|
||||||
|
fun interface Factory {
|
||||||
|
/**
|
||||||
|
* Creates a [NotificationMediaRepo].
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
fun create(
|
||||||
|
client: MatrixClient
|
||||||
|
): NotificationMediaRepo
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the file.
|
||||||
|
*
|
||||||
|
* In case of a cache hit the file is returned immediately.
|
||||||
|
* In case of a cache miss the file is downloaded and then returned.
|
||||||
|
*
|
||||||
|
* @param mediaSource the media source of the media.
|
||||||
|
* @param mimeType the mime type of the media.
|
||||||
|
* @param body the body of the message.
|
||||||
|
* @return A [Result] holding either the media [File] from the cache directory or an [Exception].
|
||||||
|
*/
|
||||||
|
suspend fun getMediaFile(
|
||||||
|
mediaSource: MediaSource,
|
||||||
|
mimeType: String?,
|
||||||
|
body: String?,
|
||||||
|
): Result<File>
|
||||||
|
}
|
||||||
|
|
||||||
|
class DefaultNotificationMediaRepo @AssistedInject constructor(
|
||||||
|
@CacheDirectory private val cacheDir: File,
|
||||||
|
private val mxcTools: MxcTools,
|
||||||
|
@Assisted private val client: MatrixClient,
|
||||||
|
) : NotificationMediaRepo {
|
||||||
|
|
||||||
|
@ContributesBinding(AppScope::class)
|
||||||
|
@AssistedFactory
|
||||||
|
fun interface Factory : NotificationMediaRepo.Factory {
|
||||||
|
override fun create(
|
||||||
|
client: MatrixClient,
|
||||||
|
): DefaultNotificationMediaRepo
|
||||||
|
}
|
||||||
|
|
||||||
|
private val matrixMediaLoader = client.mediaLoader
|
||||||
|
|
||||||
|
override suspend fun getMediaFile(
|
||||||
|
mediaSource: MediaSource,
|
||||||
|
mimeType: String?,
|
||||||
|
body: String?,
|
||||||
|
): Result<File> {
|
||||||
|
val cachedFile = mediaSource.cachedFile()
|
||||||
|
return when {
|
||||||
|
cachedFile == null -> Result.failure(IllegalStateException("Invalid mxcUri."))
|
||||||
|
cachedFile.exists() -> Result.success(cachedFile)
|
||||||
|
else -> matrixMediaLoader.downloadMediaFile(
|
||||||
|
source = mediaSource,
|
||||||
|
mimeType = mimeType,
|
||||||
|
body = body,
|
||||||
|
).mapCatching {
|
||||||
|
it.use { mediaFile ->
|
||||||
|
val dest = cachedFile.apply { parentFile?.mkdirs() }
|
||||||
|
if (mediaFile.persist(dest.path)) {
|
||||||
|
dest
|
||||||
|
} else {
|
||||||
|
error("Failed to move file to cache.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun MediaSource.cachedFile(): File? = mxcTools.mxcUri2FilePath(url)?.let {
|
||||||
|
File("${cacheDir.path}/$CACHE_NOTIFICATION_SUBDIR/$it")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subdirectory of the application's cache directory where file are stored.
|
||||||
|
*/
|
||||||
|
private const val CACHE_NOTIFICATION_SUBDIR = "temp/notif"
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
/*
|
||||||
|
* 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 androidx.core.content.FileProvider
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We have to declare our own file provider to avoid collision with other modules
|
||||||
|
* having their own.
|
||||||
|
*/
|
||||||
|
class NotificationsFileProvider : FileProvider()
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2020 New Vector Ltd
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
|
|
||||||
|
|
@ -58,9 +58,8 @@ data class NotifiableMessageEvent(
|
||||||
override val description: String = body ?: ""
|
override val description: String = body ?: ""
|
||||||
val title: String = senderName ?: ""
|
val title: String = senderName ?: ""
|
||||||
|
|
||||||
// TODO EAx The image has to be downloaded and expose using the file provider.
|
// Example of value:
|
||||||
// Example of value from Element Android:
|
// content://io.element.android.x.debug.notifications.fileprovider/downloads/temp/notif/matrix.org/XGItzSDOnSyXjYtOPfiKexDJ
|
||||||
// content://im.vector.app.debug.mx-sdk.fileprovider/downloads/downloads/816abf76d806c768760568952b1862c8/F/72c33edd23dee3b95f4d5a18aa25fa54/image.png
|
|
||||||
val imageUri: Uri?
|
val imageUri: Uri?
|
||||||
get() = imageUriString?.let { Uri.parse(it) }
|
get() = imageUriString?.let { Uri.parse(it) }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<paths>
|
||||||
|
<cache-path
|
||||||
|
name="downloads"
|
||||||
|
path="/" />
|
||||||
|
</paths>
|
||||||
|
|
@ -42,6 +42,7 @@ import io.element.android.libraries.matrix.test.A_USER_ID_2
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClientProvider
|
import io.element.android.libraries.matrix.test.FakeMatrixClientProvider
|
||||||
import io.element.android.libraries.matrix.test.notification.FakeNotificationService
|
import io.element.android.libraries.matrix.test.notification.FakeNotificationService
|
||||||
|
import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationMediaRepo
|
||||||
import io.element.android.libraries.push.impl.notifications.model.FallbackNotifiableEvent
|
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.InviteNotifiableEvent
|
||||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
|
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
|
||||||
|
|
@ -257,7 +258,7 @@ class NotifiableEventResolverTest {
|
||||||
createNotificationData(
|
createNotificationData(
|
||||||
content = NotificationContent.MessageLike.RoomMessage(
|
content = NotificationContent.MessageLike.RoomMessage(
|
||||||
senderId = A_USER_ID_2,
|
senderId = A_USER_ID_2,
|
||||||
messageType = LocationMessageType("Location", "geo:1,2", null),
|
messageType = LocationMessageType("Location", "geo:1,2", null),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
@ -274,7 +275,7 @@ class NotifiableEventResolverTest {
|
||||||
createNotificationData(
|
createNotificationData(
|
||||||
content = NotificationContent.MessageLike.RoomMessage(
|
content = NotificationContent.MessageLike.RoomMessage(
|
||||||
senderId = A_USER_ID_2,
|
senderId = A_USER_ID_2,
|
||||||
messageType = NoticeMessageType("Notice", null),
|
messageType = NoticeMessageType("Notice", null),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
@ -291,7 +292,7 @@ class NotifiableEventResolverTest {
|
||||||
createNotificationData(
|
createNotificationData(
|
||||||
content = NotificationContent.MessageLike.RoomMessage(
|
content = NotificationContent.MessageLike.RoomMessage(
|
||||||
senderId = A_USER_ID_2,
|
senderId = A_USER_ID_2,
|
||||||
messageType = EmoteMessageType("is happy", null),
|
messageType = EmoteMessageType("is happy", null),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
@ -487,11 +488,15 @@ class NotifiableEventResolverTest {
|
||||||
Result.success(FakeMatrixClient(notificationService = notificationService))
|
Result.success(FakeMatrixClient(notificationService = notificationService))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
val notificationMediaRepoFactory = NotificationMediaRepo.Factory {
|
||||||
|
FakeNotificationMediaRepo()
|
||||||
|
}
|
||||||
return NotifiableEventResolver(
|
return NotifiableEventResolver(
|
||||||
stringProvider = AndroidStringProvider(context.resources),
|
stringProvider = AndroidStringProvider(context.resources),
|
||||||
clock = FakeSystemClock(),
|
clock = FakeSystemClock(),
|
||||||
matrixClientProvider = matrixClientProvider,
|
matrixClientProvider = matrixClientProvider,
|
||||||
|
notificationMediaRepoFactory = notificationMediaRepoFactory,
|
||||||
|
context = context,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -512,7 +517,6 @@ class NotifiableEventResolverTest {
|
||||||
isNoisy = false,
|
isNoisy = false,
|
||||||
timestamp = A_TIMESTAMP,
|
timestamp = A_TIMESTAMP,
|
||||||
content = content,
|
content = content,
|
||||||
contentUrl = null,
|
|
||||||
hasMention = hasMention,
|
hasMention = hasMention,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
/*
|
||||||
|
* 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.fake
|
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||||
|
import io.element.android.libraries.push.impl.notifications.NotificationMediaRepo
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class FakeNotificationMediaRepo : NotificationMediaRepo {
|
||||||
|
override suspend fun getMediaFile(
|
||||||
|
mediaSource: MediaSource,
|
||||||
|
mimeType: String?,
|
||||||
|
body: String?,
|
||||||
|
): Result<File> {
|
||||||
|
return Result.failure(IllegalStateException("Fake class"))
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue