diff --git a/CHANGES.md b/CHANGES.md index 5a0988d7cc..df477df4d4 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,20 @@ +Changes in Element v1.6.12 (2024-02-16) +======================================= + +This update provides important security fixes, please update now. + +Security fixes 🔐 +----------------- + - Add a check on incoming intent. ([#1506 internal](https://github.com/matrix-org/internal-config/issues/1506)) + - Store temporary files created for Camera in the media folder. ([#1505 internal](https://github.com/matrix-org/internal-config/issues/1505)) + +Bugfixes 🐛 +---------- + - Switch the position and styles of the 'already have an account' and 'create account' buttons in the login splash screen. Also changes the 'already have an account one' to just say 'sign in'. ([#+update-login-splash-screen](https://github.com/element-hq/element-android/issues/+update-login-splash-screen)) + - Improve `Event.getClearContent()` and fix assignment issue that may help to decrypt last Event in the room list. ([#8744](https://github.com/element-hq/element-android/issues/8744)) + - Fix issues about location Event avatar rendering. ([#8749](https://github.com/element-hq/element-android/issues/8749)) + + Changes in Element v1.6.10 (2024-01-09) ======================================= diff --git a/SECURITY.md b/SECURITY.md index 3126b47a07..618ed65317 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,5 +1,5 @@ # Reporting a Vulnerability -**If you've found a security vulnerability, please report it to security@matrix.org** +**If you've found a security vulnerability in Element software, please report it to security@element.io.** -For more information on our security disclosure policy, visit https://www.matrix.org/security-disclosure-policy/ +For more information on our security disclosure policy, visit https://element.io/security/security-disclosure-policy. diff --git a/dependencies.gradle b/dependencies.gradle index 79368a8977..8b9125d9e8 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -101,7 +101,7 @@ ext.libs = [ ], element : [ 'opusencoder' : "io.element.android:opusencoder:1.1.0", - 'wysiwyg' : "io.element.android:wysiwyg:2.24.0" + 'wysiwyg' : "io.element.android:wysiwyg:2.29.0" ], squareup : [ 'moshi' : "com.squareup.moshi:moshi:$moshi", diff --git a/fastlane/metadata/android/en-US/changelogs/40106100.txt b/fastlane/metadata/android/en-US/changelogs/40106100.txt index 778f0b40c5..f9df252025 100644 --- a/fastlane/metadata/android/en-US/changelogs/40106100.txt +++ b/fastlane/metadata/android/en-US/changelogs/40106100.txt @@ -1,2 +1,2 @@ -Main changes in this version: add Mobile Device Managament and functional members support. +Main changes in this version: add Mobile Device Management and functional members support. Full changelog: https://github.com/element-hq/element-android/releases diff --git a/fastlane/metadata/android/en-US/changelogs/40106120.txt b/fastlane/metadata/android/en-US/changelogs/40106120.txt new file mode 100644 index 0000000000..4b10f5fdc0 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/40106120.txt @@ -0,0 +1,2 @@ +Main changes in this version: Security release. +Full changelog: https://github.com/element-hq/element-android/releases diff --git a/library/multipicker/src/main/java/im/vector/lib/multipicker/utils/MediaFileUtils.kt b/library/multipicker/src/main/java/im/vector/lib/multipicker/utils/MediaFileUtils.kt index a029d5e6b1..5b40c4a789 100644 --- a/library/multipicker/src/main/java/im/vector/lib/multipicker/utils/MediaFileUtils.kt +++ b/library/multipicker/src/main/java/im/vector/lib/multipicker/utils/MediaFileUtils.kt @@ -24,7 +24,7 @@ import java.util.Locale internal fun createTemporaryMediaFile(context: Context, mediaType: MediaType): File { val timeStamp: String = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date()) - val storageDir: File = context.filesDir.also { it.mkdirs() } + val storageDir: File = File(context.filesDir, "media").also { it.mkdirs() } val fileSuffix = when (mediaType) { MediaType.IMAGE -> ".jpg" MediaType.VIDEO -> ".mp4" diff --git a/library/multipicker/src/main/res/xml/multipicker_provider_paths.xml b/library/multipicker/src/main/res/xml/multipicker_provider_paths.xml index ff9b81ce98..fd5f866c2b 100644 --- a/library/multipicker/src/main/res/xml/multipicker_provider_paths.xml +++ b/library/multipicker/src/main/res/xml/multipicker_provider_paths.xml @@ -2,5 +2,5 @@ - \ No newline at end of file + path="media" /> + diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml index 9f842a5741..577101b0d3 100644 --- a/library/ui-strings/src/main/res/values/strings.xml +++ b/library/ui-strings/src/main/res/values/strings.xml @@ -2095,6 +2095,7 @@ Keep conversations private with encryption Extend & customize your experience Get started + Sign In Create account I already have an account diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index 84b05a527a..f46fe81432 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -62,7 +62,7 @@ android { // that the app's state is completely cleared between tests. testInstrumentationRunnerArguments clearPackageData: 'true' - buildConfigField "String", "SDK_VERSION", "\"1.6.10\"" + buildConfigField "String", "SDK_VERSION", "\"1.6.12\"" buildConfigField "String", "GIT_SDK_REVISION", "\"${gitRevision()}\"" buildConfigField "String", "GIT_SDK_REVISION_UNIX_DATE", "\"${gitRevisionUnixDate()}\"" diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/SessionExtensions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/SessionExtensions.kt index 96dac27618..267e832d1e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/SessionExtensions.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/SessionExtensions.kt @@ -19,6 +19,7 @@ package org.matrix.android.sdk.api.session import org.matrix.android.sdk.api.session.room.Room import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.user.model.User +import timber.log.Timber /** * Get a room using the RoomService of a Session. @@ -41,4 +42,5 @@ fun Session.getUser(userId: String): User? = userService().getUser(userId) /** * Similar to [getUser], but fallback to a User without details if the User is not known by the SDK, or if Session is null. */ -fun Session?.getUserOrDefault(userId: String): User = this?.userService()?.getUser(userId) ?: User(userId) +fun Session?.getUserOrDefault(userId: String): User = this?.userService()?.getUser(userId) + ?: User(userId).also { Timber.w("User $userId not found in local cache, fallback to default") } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt index bad8b3766d..196b419598 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt @@ -219,10 +219,16 @@ data class Event( } /** - * @return the event content + * @return the event content. + * If the content is encrypted, it will return the decrypted content, or null if the content is not + * decrypted. */ fun getClearContent(): Content? { - return getDecryptedContent() ?: content + return if (isEncrypted()) { + getDecryptedContent() + } else { + content + } } /** diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/livelocation/LiveLocationShareAggregatedSummary.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/livelocation/LiveLocationShareAggregatedSummary.kt index 5ad1a48217..8d0a95ad13 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/livelocation/LiveLocationShareAggregatedSummary.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/livelocation/LiveLocationShareAggregatedSummary.kt @@ -22,6 +22,7 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconLocati * Aggregation info concerning a live location share. */ data class LiveLocationShareAggregatedSummary( + val roomId: String?, val userId: String?, /** * Indicate whether the live is currently running. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RustCryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RustCryptoService.kt index d743631264..c0407ca4e8 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RustCryptoService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RustCryptoService.kt @@ -627,7 +627,7 @@ internal class RustCryptoService @Inject constructor( } private fun notifyRoomKeyReceived( - roomId: String, + roomId: String?, sessionId: String, ) { megolmSessionImportManager.dispatchNewSession(roomId, sessionId) @@ -664,9 +664,9 @@ internal class RustCryptoService @Inject constructor( when (event.type) { EventType.ROOM_KEY -> { val content = event.getClearContent().toModel() ?: return@forEach - content.sessionKey - val roomId = content.sessionId ?: return@forEach - val sessionId = content.sessionId + + val roomId = content.roomId + val sessionId = content.sessionId ?: return@forEach notifyRoomKeyReceived(roomId, sessionId) matrixConfiguration.cryptoAnalyticsPlugin?.onRoomKeyImported(sessionId, EventType.ROOM_KEY) @@ -674,8 +674,8 @@ internal class RustCryptoService @Inject constructor( EventType.FORWARDED_ROOM_KEY -> { val content = event.getClearContent().toModel() ?: return@forEach - val roomId = content.sessionId ?: return@forEach - val sessionId = content.sessionId + val roomId = content.roomId + val sessionId = content.sessionId ?: return@forEach notifyRoomKeyReceived(roomId, sessionId) matrixConfiguration.cryptoAnalyticsPlugin?.onRoomKeyImported(sessionId, EventType.FORWARDED_ROOM_KEY) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/LiveLocationShareAggregatedSummaryMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/LiveLocationShareAggregatedSummaryMapper.kt index 4a4c730a0b..61f4450e8f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/LiveLocationShareAggregatedSummaryMapper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/LiveLocationShareAggregatedSummaryMapper.kt @@ -28,6 +28,7 @@ internal class LiveLocationShareAggregatedSummaryMapper @Inject constructor() : override fun map(entity: LiveLocationShareAggregatedSummaryEntity): LiveLocationShareAggregatedSummary { return LiveLocationShareAggregatedSummary( + roomId = entity.roomId, userId = entity.userId, isActive = entity.isActive, endOfLiveTimestampMillis = entity.endOfLiveTimestampMillis, diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/database/mapper/LiveLocationShareAggregatedSummaryMapperTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/database/mapper/LiveLocationShareAggregatedSummaryMapperTest.kt index 47d5f46525..94264ffe43 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/database/mapper/LiveLocationShareAggregatedSummaryMapperTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/database/mapper/LiveLocationShareAggregatedSummaryMapperTest.kt @@ -24,6 +24,7 @@ import org.matrix.android.sdk.api.session.room.model.message.LocationInfo import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconLocationDataContent import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity +private const val ANY_ROOM_ID = "a-room-id" private const val ANY_USER_ID = "a-user-id" private const val ANY_ACTIVE_STATE = true private const val ANY_TIMEOUT = 123L @@ -40,6 +41,7 @@ class LiveLocationShareAggregatedSummaryMapperTest { val summary = mapper.map(entity) summary shouldBeEqualTo LiveLocationShareAggregatedSummary( + roomId = ANY_ROOM_ID, userId = ANY_USER_ID, isActive = ANY_ACTIVE_STATE, endOfLiveTimestampMillis = ANY_TIMEOUT, @@ -48,6 +50,7 @@ class LiveLocationShareAggregatedSummaryMapperTest { } private fun anEntity(content: MessageBeaconLocationDataContent) = LiveLocationShareAggregatedSummaryEntity( + roomId = ANY_ROOM_ID, userId = ANY_USER_ID, isActive = ANY_ACTIVE_STATE, endOfLiveTimestampMillis = ANY_TIMEOUT, diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/event/ValidDecryptedEventTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/event/ValidDecryptedEventTest.kt index 5fda242b90..534d7ba6ad 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/event/ValidDecryptedEventTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/event/ValidDecryptedEventTest.kt @@ -89,7 +89,7 @@ class ValidDecryptedEventTest { ).toContent() ) - val unValidatedContent = mixedEvent.getClearContent().toModel() + val unValidatedContent = mixedEvent.content.toModel() unValidatedContent?.body shouldBe "some message" mixedEvent.toValidDecryptedEvent()?.clearContent?.toModel() shouldBe null diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingServiceTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingServiceTest.kt index 1f15a9bee8..5c27562c9c 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingServiceTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingServiceTest.kt @@ -229,6 +229,7 @@ internal class DefaultLocationSharingServiceTest { fun `livedata of live summaries is correctly computed`() { val entity = LiveLocationShareAggregatedSummaryEntity() val summary = LiveLocationShareAggregatedSummary( + roomId = A_ROOM_ID, userId = "", isActive = true, endOfLiveTimestampMillis = 123, @@ -255,6 +256,7 @@ internal class DefaultLocationSharingServiceTest { fun `given an event id when getting livedata on corresponding live summary then it is correctly computed`() { val entity = LiveLocationShareAggregatedSummaryEntity() val summary = LiveLocationShareAggregatedSummary( + roomId = A_ROOM_ID, userId = "", isActive = true, endOfLiveTimestampMillis = 123, diff --git a/tools/release/download_github_artifacts.py b/tools/release/download_github_artifacts.py index 4424c0e009..8bc0c2d9c9 100755 --- a/tools/release/download_github_artifacts.py +++ b/tools/release/download_github_artifacts.py @@ -16,11 +16,12 @@ # import argparse -import hashlib import json import os # Run `pip3 install requests` if not installed yet import requests +# Run `pip3 install re` if not installed yet +import re # This script downloads artifacts from GitHub. # Ref: https://docs.github.com/en/rest/actions/artifacts#get-an-artifact @@ -65,22 +66,20 @@ if args.verbose: print(args) # Split the artifact URL to get information -# Ex: https://github.com/element-hq/element-android/suites/9293388174/artifacts/435942121 +# Ex: https://github.com/element-hq/element-android/actions/runs/7460386865/artifacts/1156548729 artifactUrl = args.artifactUrl -if not artifactUrl.startswith('https://github.com/'): - print("❌ Invalid parameter --artifactUrl %s. Must start with 'https://github.com/'" % artifactUrl) - exit(1) -if "/artifacts/" not in artifactUrl: - print("❌ Invalid parameter --artifactUrl %s. Must contain '/artifacts/'" % artifactUrl) - exit(1) -artifactItems = artifactUrl.split("/") -if len(artifactItems) != 9: - print("❌ Invalid parameter --artifactUrl %s. Please check the format." % (artifactUrl)) + +url_regex = r"https://github.com/(.+?)/(.+?)/actions/runs/.+?/artifacts/(.+)" +result = re.search(url_regex, artifactUrl) + +if result is None: + print( + "❌ Invalid parameter --artifactUrl '%s'. Please check the format.\nIt should be something like: %s" % + (artifactUrl, 'https://github.com/element-hq/element-android/actions/runs/7460386865/artifacts/1156548729') + ) exit(1) -gitHubRepoOwner = artifactItems[3] -gitHubRepo = artifactItems[4] -artifactId = artifactItems[8] +(gitHubRepoOwner, gitHubRepo, artifactId) = result.groups() if args.verbose: print("gitHubRepoOwner: %s, gitHubRepo: %s, artifactId: %s" % (gitHubRepoOwner, gitHubRepo, artifactId)) diff --git a/vector-app/build.gradle b/vector-app/build.gradle index 7c51dcd34c..a5df2ae1d5 100644 --- a/vector-app/build.gradle +++ b/vector-app/build.gradle @@ -37,7 +37,7 @@ ext.versionMinor = 6 // Note: even values are reserved for regular release, odd values for hotfix release. // When creating a hotfix, you should decrease the value, since the current value // is the value for the next regular release. -ext.versionPatch = 10 +ext.versionPatch = 12 static def getGitTimestamp() { def cmd = 'git show -s --format=%ct' diff --git a/vector/src/main/java/im/vector/app/core/epoxy/bottomsheet/BottomSheetMessagePreviewItem.kt b/vector/src/main/java/im/vector/app/core/epoxy/bottomsheet/BottomSheetMessagePreviewItem.kt index bb1b0fbd7b..cf5e590081 100644 --- a/vector/src/main/java/im/vector/app/core/epoxy/bottomsheet/BottomSheetMessagePreviewItem.kt +++ b/vector/src/main/java/im/vector/app/core/epoxy/bottomsheet/BottomSheetMessagePreviewItem.kt @@ -103,10 +103,10 @@ abstract class BottomSheetMessagePreviewItem : VectorEpoxyModel - GlideApp.with(holder.staticMapPinImageView) - .load(pinDrawable) - .into(holder.staticMapPinImageView) + val pinMatrixItem = matrixItem.takeIf { safeLocationUiData.locationOwnerId != null } + safeLocationUiData.locationPinProvider.create(pinMatrixItem) { pinDrawable -> + // we are not using Glide since it does not display it correctly when there is no user photo + holder.staticMapPinImageView.setImageDrawable(pinDrawable) } } } diff --git a/vector/src/main/java/im/vector/app/features/MainActivity.kt b/vector/src/main/java/im/vector/app/features/MainActivity.kt index c8aa108ccb..0f0ac83903 100644 --- a/vector/src/main/java/im/vector/app/features/MainActivity.kt +++ b/vector/src/main/java/im/vector/app/features/MainActivity.kt @@ -39,6 +39,9 @@ import im.vector.app.features.analytics.VectorAnalytics import im.vector.app.features.analytics.plan.ViewRoom import im.vector.app.features.home.HomeActivity import im.vector.app.features.home.ShortcutsHandler +import im.vector.app.features.home.room.detail.RoomDetailActivity +import im.vector.app.features.home.room.threads.ThreadsActivity +import im.vector.app.features.location.live.map.LiveLocationMapViewActivity import im.vector.app.features.notifications.NotificationDrawerManager import im.vector.app.features.pin.UnlockedActivity import im.vector.app.features.pin.lockscreen.crypto.LockScreenKeyRepository @@ -115,6 +118,14 @@ class MainActivity : VectorBaseActivity(), UnlockedActivity putExtra(EXTRA_ROOM_ID, roomId) } } + + val allowList = listOf( + HomeActivity::class.java.name, + MainActivity::class.java.name, + RoomDetailActivity::class.java.name, + ThreadsActivity::class.java.name, + LiveLocationMapViewActivity::class.java.name, + ) } private val startAppViewModel: StartAppViewModel by viewModel() @@ -186,6 +197,7 @@ class MainActivity : VectorBaseActivity(), UnlockedActivity // Start the next Activity startSyncing() val nextIntent = intent.getParcelableExtraCompat(EXTRA_NEXT_INTENT) + ?.takeIf { it.isValid() } startIntentAndFinish(nextIntent) } else if (intent.hasExtra(EXTRA_INIT_SESSION)) { startSyncing() @@ -380,4 +392,11 @@ class MainActivity : VectorBaseActivity(), UnlockedActivity intent?.let { startActivity(it) } finish() } + + private fun Intent.isValid(): Boolean { + val componentName = resolveActivity(packageManager) ?: return false + val packageName = componentName.packageName + val className = componentName.className + return packageName == buildMeta.applicationId && className in allowList + } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt index 307b7f4b99..7ab8862886 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt @@ -238,7 +238,7 @@ class MessageActionsEpoxyController @Inject constructor( val locationUrl = locationContent.toLocationData() ?.let { urlMapProvider.buildStaticMapUrl(it, INITIAL_MAP_ZOOM_IN_TIMELINE, 1200, 800) } ?: return null - val locationOwnerId = if (locationContent.isSelfLocation()) state.informationData.matrixItem.id else null + val locationOwnerId = if (locationContent.isSelfLocation()) state.informationData.senderId else null return LocationUiData( locationUrl = locationUrl, diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/LiveLocationShareMessageItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/LiveLocationShareMessageItemFactory.kt index 493602a291..8482f0e2b6 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/LiveLocationShareMessageItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/LiveLocationShareMessageItemFactory.kt @@ -114,7 +114,7 @@ class LiveLocationShareMessageItemFactory @Inject constructor( .locationUrl(locationUrl) .mapWidth(width) .mapHeight(height) - .locationUserId(attributes.informationData.senderId) + .pinMatrixItem(attributes.informationData.matrixItem) .locationPinProvider(locationPinProvider) .highlighted(highlight) .leftGuideline(avatarSizeProvider.leftGuideline) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt index dd52c05265..94189a7a53 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -233,14 +233,14 @@ class MessageItemFactory @Inject constructor( urlMapProvider.buildStaticMapUrl(it, INITIAL_MAP_ZOOM_IN_TIMELINE, width, height) } - val locationUserId = if (locationContent.isSelfLocation()) informationData.senderId else null + val pinMatrixItem = if (locationContent.isSelfLocation()) informationData.matrixItem else null return MessageLocationItem_() .attributes(attributes) .locationUrl(locationUrl) .mapWidth(width) .mapHeight(height) - .locationUserId(locationUserId) + .pinMatrixItem(pinMatrixItem) .locationPinProvider(locationPinProvider) .highlighted(highlight) .leftGuideline(avatarSizeProvider.leftGuideline) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/LocationPinProvider.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/LocationPinProvider.kt index 7f276f2f73..2fdd7198a9 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/LocationPinProvider.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/LocationPinProvider.kt @@ -19,31 +19,33 @@ package im.vector.app.features.home.room.detail.timeline.helper import android.content.Context import android.graphics.drawable.Drawable import android.graphics.drawable.LayerDrawable -import androidx.annotation.ColorInt +import android.util.LruCache import androidx.core.content.ContextCompat import androidx.core.graphics.drawable.DrawableCompat import com.bumptech.glide.request.target.CustomTarget import com.bumptech.glide.request.transition.Transition import im.vector.app.R -import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.glide.GlideApp import im.vector.app.core.utils.DimensionConverter import im.vector.app.features.home.AvatarRenderer -import org.matrix.android.sdk.api.session.getUserOrDefault -import org.matrix.android.sdk.api.util.toMatrixItem +import org.matrix.android.sdk.api.util.MatrixItem import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton +private data class CachedDrawable( + val drawable: Drawable, + val isError: Boolean, +) + @Singleton class LocationPinProvider @Inject constructor( private val context: Context, - private val activeSessionHolder: ActiveSessionHolder, private val dimensionConverter: DimensionConverter, private val avatarRenderer: AvatarRenderer, private val matrixItemColorProvider: MatrixItemColorProvider ) { - private val cache = mutableMapOf() + private val cache = LruCache(32) private val glideRequests by lazy { GlideApp.with(context) @@ -51,53 +53,51 @@ class LocationPinProvider @Inject constructor( /** * Creates a pin drawable. If userId is null then a generic pin drawable will be created. - * @param userId userId that will be used to retrieve user avatar + * @param matrixUser user that will be used to retrieve user avatar * @param callback Pin drawable will be sent through the callback */ - fun create(userId: String?, callback: (Drawable) -> Unit) { - if (userId == null) { + fun create(matrixUser: MatrixItem?, callback: (Drawable) -> Unit) { + if (matrixUser == null) { callback(ContextCompat.getDrawable(context, R.drawable.ic_location_pin)!!) return } + val size = dimensionConverter.dpToPx(44) + avatarRenderer.render(glideRequests, matrixUser, object : CustomTarget(size, size) { + override fun onResourceReady(resource: Drawable, transition: Transition?) { + Timber.d("## Location: onResourceReady") + val pinDrawable = createPinDrawable(matrixUser, resource, isError = false) + callback(pinDrawable) + } - if (cache.contains(userId)) { - callback(cache[userId]!!) - return - } + override fun onLoadCleared(placeholder: Drawable?) { + // Is it possible? Put placeholder instead? + // FIXME The doc says it has to be implemented and should free resources + Timber.d("## Location: onLoadCleared") + } - activeSessionHolder - .getActiveSession() - .getUserOrDefault(userId) - .toMatrixItem() - .let { userItem -> - val size = dimensionConverter.dpToPx(44) - val bgTintColor = matrixItemColorProvider.getColor(userItem) - avatarRenderer.render(glideRequests, userItem, object : CustomTarget(size, size) { - override fun onResourceReady(resource: Drawable, transition: Transition?) { - Timber.d("## Location: onResourceReady") - val pinDrawable = createPinDrawable(resource, bgTintColor) - cache[userId] = pinDrawable - callback(pinDrawable) - } - - override fun onLoadCleared(placeholder: Drawable?) { - // Is it possible? Put placeholder instead? - // FIXME The doc says it has to be implemented and should free resources - Timber.d("## Location: onLoadCleared") - } - - override fun onLoadFailed(errorDrawable: Drawable?) { - Timber.w("## Location: onLoadFailed") - errorDrawable ?: return - val pinDrawable = createPinDrawable(errorDrawable, bgTintColor) - cache[userId] = pinDrawable - callback(pinDrawable) - } - }) - } + override fun onLoadFailed(errorDrawable: Drawable?) { + // Note: `onLoadFailed` is also called when the user has no avatarUrl + // and the errorDrawable is actually the placeholder. + Timber.w("## Location: onLoadFailed") + errorDrawable ?: return + val pinDrawable = createPinDrawable(matrixUser, errorDrawable, isError = true) + callback(pinDrawable) + } + }) } - private fun createPinDrawable(drawable: Drawable, @ColorInt bgTintColor: Int): Drawable { + private fun createPinDrawable( + userItem: MatrixItem, + drawable: Drawable, + isError: Boolean, + ): Drawable { + val fromCache = cache.get(userItem) + // Return the cached drawable only if it is valid, or the new drawable is again an error + if (fromCache != null && (!fromCache.isError || isError)) { + return fromCache.drawable + } + + val bgTintColor = matrixItemColorProvider.getColor(userItem) val bgUserPin = ContextCompat.getDrawable(context, R.drawable.bg_map_user_pin)!! // use mutate on drawable to avoid sharing the color when we have multiple different user pins DrawableCompat.setTint(bgUserPin.mutate(), bgTintColor) @@ -106,6 +106,7 @@ class LocationPinProvider @Inject constructor( val topInset = dimensionConverter.dpToPx(4) val bottomInset = dimensionConverter.dpToPx(8) layerDrawable.setLayerInset(1, horizontalInset, topInset, horizontalInset, bottomInset) + cache.put(userItem, CachedDrawable(layerDrawable, isError)) return layerDrawable } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageLocationItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageLocationItem.kt index 4903b8c8cf..c3294d0301 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageLocationItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageLocationItem.kt @@ -38,6 +38,7 @@ import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLay import im.vector.app.features.home.room.detail.timeline.style.granularRoundedCorners import im.vector.app.features.location.MapLoadingErrorView import im.vector.app.features.location.MapLoadingErrorViewState +import org.matrix.android.sdk.api.util.MatrixItem abstract class AbsMessageLocationItem( @LayoutRes layoutId: Int = R.layout.item_timeline_event_base @@ -47,7 +48,7 @@ abstract class AbsMessageLocationItem( var locationUrl: String? = null @EpoxyAttribute - var locationUserId: String? = null + var pinMatrixItem: MatrixItem? = null @EpoxyAttribute var mapWidth: Int = 0 @@ -103,7 +104,7 @@ abstract class AbsMessageLocationItem( dataSource: DataSource?, isFirstResource: Boolean ): Boolean { - locationPinProvider?.create(locationUserId) { pinDrawable -> + locationPinProvider?.create(pinMatrixItem) { pinDrawable -> // we are not using Glide since it does not display it correctly when there is no user photo holder.staticMapPinImageView.setImageDrawable(pinDrawable) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageLiveLocationItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageLiveLocationItem.kt index 010abecc73..e1a081f450 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageLiveLocationItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageLiveLocationItem.kt @@ -49,7 +49,7 @@ abstract class MessageLiveLocationItem : AbsMessageLocationItem { - locationPinProvider.create(userId) { pinDrawable -> - val session = activeSessionHolder.getActiveSession() + val session = activeSessionHolder.getActiveSession() + val roomId = liveLocationShareAggregatedSummary.roomId + val matrixItem = if (roomId != null) { + session.getRoom(roomId) + ?.membershipService() + ?.getRoomMember(userId) + ?.toMatrixItem() + ?: MatrixItem.UserItem(userId) + } else { + session.getUserOrDefault(userId).toMatrixItem() + } + locationPinProvider.create(matrixItem) { pinDrawable -> val locationTimestampMillis = liveLocationShareAggregatedSummary.lastLocationDataContent?.getBestTimestampMillis() val viewState = UserLiveLocationViewState( - matrixItem = session.getUserOrDefault(userId).toMatrixItem(), + matrixItem = matrixItem, pinDrawable = pinDrawable, locationData = locationData, endOfLiveTimestampMillis = liveLocationShareAggregatedSummary.endOfLiveTimestampMillis, diff --git a/vector/src/main/java/im/vector/app/features/location/preview/LocationPreviewViewModel.kt b/vector/src/main/java/im/vector/app/features/location/preview/LocationPreviewViewModel.kt index a1544ac2af..4de005b265 100644 --- a/vector/src/main/java/im/vector/app/features/location/preview/LocationPreviewViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/location/preview/LocationPreviewViewModel.kt @@ -30,6 +30,8 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.util.MatrixItem +import org.matrix.android.sdk.api.util.toMatrixItem class LocationPreviewViewModel @AssistedInject constructor( @Assisted private val initialState: LocationPreviewViewState, @@ -46,12 +48,23 @@ class LocationPreviewViewModel @AssistedInject constructor( companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() init { - initPin(initialState.pinUserId) + val matrixItem = if (initialState.roomId != null && initialState.pinUserId != null) { + session + .roomService() + .getRoom(initialState.roomId) + ?.membershipService() + ?.getRoomMember(initialState.pinUserId) + ?.toMatrixItem() + ?: MatrixItem.UserItem(initialState.pinUserId) + } else { + null + } + initPin(matrixItem) initLocationTracking() } - private fun initPin(userId: String?) { - locationPinProvider.create(userId) { pinDrawable -> + private fun initPin(matrixItem: MatrixItem?) { + locationPinProvider.create(matrixItem) { pinDrawable -> setState { copy(pinDrawable = pinDrawable) } } } diff --git a/vector/src/main/java/im/vector/app/features/location/preview/LocationPreviewViewState.kt b/vector/src/main/java/im/vector/app/features/location/preview/LocationPreviewViewState.kt index 23f8d4d7dc..81ea6ebdce 100644 --- a/vector/src/main/java/im/vector/app/features/location/preview/LocationPreviewViewState.kt +++ b/vector/src/main/java/im/vector/app/features/location/preview/LocationPreviewViewState.kt @@ -23,6 +23,7 @@ import im.vector.app.features.location.LocationSharingArgs data class LocationPreviewViewState( val pinLocationData: LocationData? = null, + val roomId: String? = null, val pinUserId: String? = null, val pinDrawable: Drawable? = null, val loadingMapHasFailed: Boolean = false, @@ -32,6 +33,7 @@ data class LocationPreviewViewState( constructor(args: LocationSharingArgs) : this( pinLocationData = args.initialLocationData, + roomId = args.roomId, pinUserId = args.locationOwnerId, ) } diff --git a/vector/src/main/res/layout/fragment_ftue_splash_carousel.xml b/vector/src/main/res/layout/fragment_ftue_splash_carousel.xml index c6b27f5d09..bb44af1221 100644 --- a/vector/src/main/res/layout/fragment_ftue_splash_carousel.xml +++ b/vector/src/main/res/layout/fragment_ftue_splash_carousel.xml @@ -57,30 +57,30 @@ app:layout_constraintTop_toBottomOf="@id/carouselIndicator" />