Merge branch 'release/1.6.12' into main

This commit is contained in:
Benoit Marty 2024-02-16 11:56:27 +01:00
commit effdca1832
37 changed files with 203 additions and 107 deletions

View file

@ -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)
=======================================

View file

@ -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.

View file

@ -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",

View file

@ -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

View file

@ -0,0 +1,2 @@
Main changes in this version: Security release.
Full changelog: https://github.com/element-hq/element-android/releases

View file

@ -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"

View file

@ -2,5 +2,5 @@
<paths>
<files-path
name="external_files"
path="." />
path="media" />
</paths>

View file

@ -2095,6 +2095,7 @@
<string name="login_splash_text2">Keep conversations private with encryption</string>
<string name="login_splash_text3">Extend &amp; customize your experience</string>
<string name="login_splash_submit">Get started</string>
<string name="login_splash_sign_in">Sign In</string>
<string name="login_splash_create_account">Create account</string>
<string name="login_splash_already_have_account">I already have an account</string>

View file

@ -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()}\""

View file

@ -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") }

View file

@ -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
}
}
/**

View file

@ -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.

View file

@ -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<RoomKeyContent>() ?: 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<ForwardedRoomKeyContent>() ?: 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)

View file

@ -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,

View file

@ -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,

View file

@ -89,7 +89,7 @@ class ValidDecryptedEventTest {
).toContent()
)
val unValidatedContent = mixedEvent.getClearContent().toModel<MessageTextContent>()
val unValidatedContent = mixedEvent.content.toModel<MessageTextContent>()
unValidatedContent?.body shouldBe "some message"
mixedEvent.toValidDecryptedEvent()?.clearContent?.toModel<MessageTextContent>() shouldBe null

View file

@ -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,

View file

@ -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))

View file

@ -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'

View file

@ -103,10 +103,10 @@ abstract class BottomSheetMessagePreviewItem : VectorEpoxyModel<BottomSheetMessa
.apply(RequestOptions.centerCropTransform())
.into(holder.staticMapImageView)
safeLocationUiData.locationPinProvider.create(safeLocationUiData.locationOwnerId) { pinDrawable ->
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)
}
}
}

View file

@ -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<ActivityMainBinding>(), 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<ActivityMainBinding>(), UnlockedActivity
// Start the next Activity
startSyncing()
val nextIntent = intent.getParcelableExtraCompat<Intent>(EXTRA_NEXT_INTENT)
?.takeIf { it.isValid() }
startIntentAndFinish(nextIntent)
} else if (intent.hasExtra(EXTRA_INIT_SESSION)) {
startSyncing()
@ -380,4 +392,11 @@ class MainActivity : VectorBaseActivity<ActivityMainBinding>(), 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
}
}

View file

@ -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,

View file

@ -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)

View file

@ -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)

View file

@ -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<String, Drawable>()
private val cache = LruCache<MatrixItem, CachedDrawable>(32)
private val glideRequests by lazy {
GlideApp.with(context)
@ -51,32 +53,19 @@ 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
}
if (cache.contains(userId)) {
callback(cache[userId]!!)
return
}
activeSessionHolder
.getActiveSession()
.getUserOrDefault(userId)
.toMatrixItem()
.let { userItem ->
val size = dimensionConverter.dpToPx(44)
val bgTintColor = matrixItemColorProvider.getColor(userItem)
avatarRenderer.render(glideRequests, userItem, object : CustomTarget<Drawable>(size, size) {
avatarRenderer.render(glideRequests, matrixUser, object : CustomTarget<Drawable>(size, size) {
override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) {
Timber.d("## Location: onResourceReady")
val pinDrawable = createPinDrawable(resource, bgTintColor)
cache[userId] = pinDrawable
val pinDrawable = createPinDrawable(matrixUser, resource, isError = false)
callback(pinDrawable)
}
@ -87,17 +76,28 @@ class LocationPinProvider @Inject constructor(
}
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(errorDrawable, bgTintColor)
cache[userId] = pinDrawable
val pinDrawable = createPinDrawable(matrixUser, errorDrawable, isError = true)
callback(pinDrawable)
}
})
}
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
}
private fun createPinDrawable(drawable: Drawable, @ColorInt bgTintColor: Int): 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
}
}

View file

@ -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<H : AbsMessageLocationItem.Holder>(
@LayoutRes layoutId: Int = R.layout.item_timeline_event_base
@ -47,7 +48,7 @@ abstract class AbsMessageLocationItem<H : AbsMessageLocationItem.Holder>(
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<H : AbsMessageLocationItem.Holder>(
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)
}

View file

@ -49,7 +49,7 @@ abstract class MessageLiveLocationItem : AbsMessageLocationItem<MessageLiveLocat
private fun bindLiveLocationBanner(holder: Holder) {
// TODO in a future PR add check on device id to confirm that is the one that sent the beacon
val isEmitter = currentUserId != null && currentUserId == locationUserId
val isEmitter = currentUserId != null && currentUserId == pinMatrixItem?.id
val messageLayout = attributes.informationData.messageLayout
val viewState = buildViewState(holder, messageLayout, isEmitter)
holder.liveLocationRunningBanner.isVisible = true

View file

@ -106,11 +106,13 @@ class LocationSharingViewModel @AssistedInject constructor(
private fun updatePin(isUserPin: Boolean? = true) {
if (isUserPin.orFalse()) {
locationPinProvider.create(userId = session.myUserId) {
val matrixItem = room.membershipService().getRoomMember(session.myUserId)?.toMatrixItem()
?: session.getUserOrDefault(session.myUserId).toMatrixItem()
locationPinProvider.create(matrixItem) {
updatePinDrawableInState(it)
}
} else {
locationPinProvider.create(userId = null) {
locationPinProvider.create(null) {
updatePinDrawableInState(it)
}
}

View file

@ -20,8 +20,10 @@ import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.features.home.room.detail.timeline.helper.LocationPinProvider
import im.vector.app.features.location.toLocationData
import kotlinx.coroutines.suspendCancellableCoroutine
import org.matrix.android.sdk.api.session.getRoom
import org.matrix.android.sdk.api.session.getUserOrDefault
import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationShareAggregatedSummary
import org.matrix.android.sdk.api.util.MatrixItem
import org.matrix.android.sdk.api.util.toMatrixItem
import javax.inject.Inject
@ -43,11 +45,21 @@ class UserLiveLocationViewStateMapper @Inject constructor(
// do nothing on cancellation
}
else -> {
locationPinProvider.create(userId) { pinDrawable ->
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,

View file

@ -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<LocationPreviewViewModel, LocationPreviewViewState> 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) }
}
}

View file

@ -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,
)
}

View file

@ -57,30 +57,30 @@
app:layout_constraintTop_toBottomOf="@id/carouselIndicator" />
<Button
android:id="@+id/loginSplashSubmit"
android:id="@+id/loginSplashAlreadyHaveAccount"
style="@style/Widget.Vector.Button.Login"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/login_splash_sign_in"
android:textAllCaps="true"
android:transitionName="loginSubmitTransition"
app:layout_constraintBottom_toTopOf="@id/loginSplashAlreadyHaveAccount"
app:layout_constraintBottom_toTopOf="@id/loginSplashSubmit"
app:layout_constraintEnd_toEndOf="@id/splashCarouselGutterEnd"
app:layout_constraintStart_toStartOf="@id/splashCarouselGutterStart"
app:layout_constraintTop_toBottomOf="@id/loginSplashButtonsSpace"
tools:text="@string/login_splash_create_account" />
app:layout_constraintTop_toBottomOf="@id/loginSplashButtonsSpace" />
<Button
android:id="@+id/loginSplashAlreadyHaveAccount"
android:id="@+id/loginSplashSubmit"
style="@style/Widget.Vector.Button.Text.Login"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/login_splash_already_have_account"
android:textAllCaps="true"
android:transitionName="loginSubmitTransition"
app:layout_constraintBottom_toTopOf="@id/loginSplashBottomSpace"
app:layout_constraintEnd_toEndOf="@id/splashCarouselGutterEnd"
app:layout_constraintStart_toStartOf="@id/splashCarouselGutterStart"
app:layout_constraintTop_toBottomOf="@id/loginSplashSubmit" />
app:layout_constraintTop_toBottomOf="@id/loginSplashAlreadyHaveAccount"
tools:text="@string/login_splash_create_account" />
<Space
android:id="@+id/loginSplashBottomSpace"

View file

@ -17,6 +17,7 @@
package im.vector.app.features.location
import org.amshove.kluent.shouldBeEqualTo
import org.amshove.kluent.shouldBeFalse
import org.amshove.kluent.shouldBeNull
import org.amshove.kluent.shouldBeTrue
import org.junit.Test
@ -80,6 +81,9 @@ class LocationDataTest {
val contentWithSelfAssetType = MessageLocationContent(body = "", geoUri = "", unstableLocationAsset = LocationAsset(type = LocationAssetType.SELF))
contentWithSelfAssetType.isSelfLocation().shouldBeTrue()
val contentWithPinAssetType = MessageLocationContent(body = "", geoUri = "", unstableLocationAsset = LocationAsset(type = LocationAssetType.PIN))
contentWithPinAssetType.isSelfLocation().shouldBeFalse()
}
@Test

View file

@ -54,6 +54,7 @@ class GetLiveLocationShareSummaryUseCaseTest {
@Test
fun `given a room id and event id when calling use case then flow on summary is returned`() = runTest {
val summary = LiveLocationShareAggregatedSummary(
roomId = A_ROOM_ID,
userId = "userId",
isActive = true,
endOfLiveTimestampMillis = 123,

View file

@ -59,18 +59,21 @@ class GetListOfUserLiveLocationUseCaseTest {
@Test
fun `given a room id then the correct flow of view states list is collected`() = runTest {
val summary1 = LiveLocationShareAggregatedSummary(
roomId = A_ROOM_ID,
userId = "userId1",
isActive = true,
endOfLiveTimestampMillis = 123,
lastLocationDataContent = MessageBeaconLocationDataContent()
)
val summary2 = LiveLocationShareAggregatedSummary(
roomId = A_ROOM_ID,
userId = "userId2",
isActive = true,
endOfLiveTimestampMillis = 1234,
lastLocationDataContent = MessageBeaconLocationDataContent()
)
val summary3 = LiveLocationShareAggregatedSummary(
roomId = A_ROOM_ID,
userId = "userId3",
isActive = true,
endOfLiveTimestampMillis = 1234,

View file

@ -72,6 +72,7 @@ class UserLiveLocationViewStateMapperTest {
@Test
fun `given a summary with invalid data then result is null`() = runTest {
val summary1 = LiveLocationShareAggregatedSummary(
roomId = null,
userId = null,
isActive = true,
endOfLiveTimestampMillis = null,
@ -98,17 +99,19 @@ class UserLiveLocationViewStateMapperTest {
unstableTimestampMillis = A_LOCATION_TIMESTAMP
)
val summary = LiveLocationShareAggregatedSummary(
roomId = null,
userId = A_USER_ID,
isActive = A_IS_ACTIVE,
endOfLiveTimestampMillis = A_END_OF_LIVE_TIMESTAMP,
lastLocationDataContent = locationDataContent,
)
locationPinProvider.givenCreateForUserId(A_USER_ID, pinDrawable)
val matrixItem = MatrixItem.UserItem(id = A_USER_ID, displayName = A_USER_DISPLAY_NAME, avatarUrl = "")
locationPinProvider.givenCreateForMatrixItem(matrixItem, pinDrawable)
val viewState = userLiveLocationViewStateMapper.map(summary)
val expectedViewState = UserLiveLocationViewState(
matrixItem = MatrixItem.UserItem(id = A_USER_ID, displayName = A_USER_DISPLAY_NAME, avatarUrl = ""),
matrixItem = matrixItem,
pinDrawable = pinDrawable,
locationData = LocationData(
latitude = A_LATITUDE,

View file

@ -21,12 +21,13 @@ import im.vector.app.features.home.room.detail.timeline.helper.LocationPinProvid
import io.mockk.every
import io.mockk.invoke
import io.mockk.mockk
import org.matrix.android.sdk.api.util.MatrixItem
class FakeLocationPinProvider {
val instance = mockk<LocationPinProvider>(relaxed = true)
fun givenCreateForUserId(userId: String, expectedDrawable: Drawable) {
every { instance.create(userId, captureLambda()) } answers { lambda<(Drawable) -> Unit>().invoke(expectedDrawable) }
fun givenCreateForMatrixItem(matrixItem: MatrixItem, expectedDrawable: Drawable) {
every { instance.create(matrixItem, captureLambda()) } answers { lambda<(Drawable) -> Unit>().invoke(expectedDrawable) }
}
}