Merge pull request #6129 from vector-im/feature/mna/PSF-1019-user-pins
[Location sharing] - Show user live location pins in map view (PSF-1019)
This commit is contained in:
commit
eeaf9fd616
@ -1 +1,2 @@
|
|||||||
Live location sharing: navigation from timeline to map screen
|
Live location sharing: navigation from timeline to map screen
|
||||||
|
Live location sharing: show user pins on map screen
|
||||||
|
@ -22,6 +22,7 @@ import org.matrix.android.sdk.api.session.room.accountdata.RoomAccountDataServic
|
|||||||
import org.matrix.android.sdk.api.session.room.alias.AliasService
|
import org.matrix.android.sdk.api.session.room.alias.AliasService
|
||||||
import org.matrix.android.sdk.api.session.room.call.RoomCallService
|
import org.matrix.android.sdk.api.session.room.call.RoomCallService
|
||||||
import org.matrix.android.sdk.api.session.room.crypto.RoomCryptoService
|
import org.matrix.android.sdk.api.session.room.crypto.RoomCryptoService
|
||||||
|
import org.matrix.android.sdk.api.session.room.location.LocationSharingService
|
||||||
import org.matrix.android.sdk.api.session.room.members.MembershipService
|
import org.matrix.android.sdk.api.session.room.members.MembershipService
|
||||||
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
||||||
import org.matrix.android.sdk.api.session.room.model.relation.RelationService
|
import org.matrix.android.sdk.api.session.room.model.relation.RelationService
|
||||||
@ -163,4 +164,9 @@ interface Room {
|
|||||||
* Get the RoomVersionService associated to this Room.
|
* Get the RoomVersionService associated to this Room.
|
||||||
*/
|
*/
|
||||||
fun roomVersionService(): RoomVersionService
|
fun roomVersionService(): RoomVersionService
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the LocationSharingService associated to this Room.
|
||||||
|
*/
|
||||||
|
fun locationSharingService(): LocationSharingService
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,27 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* 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 org.matrix.android.sdk.api.session.room.location
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationShareAggregatedSummary
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manage all location sharing related features.
|
||||||
|
*/
|
||||||
|
interface LocationSharingService {
|
||||||
|
fun getRunningLiveLocationShareSummaries(): LiveData<List<LiveLocationShareAggregatedSummary>>
|
||||||
|
}
|
@ -22,6 +22,7 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconLocati
|
|||||||
* Aggregation info concerning a live location share.
|
* Aggregation info concerning a live location share.
|
||||||
*/
|
*/
|
||||||
data class LiveLocationShareAggregatedSummary(
|
data class LiveLocationShareAggregatedSummary(
|
||||||
|
val userId: String?,
|
||||||
/**
|
/**
|
||||||
* Indicate whether the live is currently running.
|
* Indicate whether the live is currently running.
|
||||||
*/
|
*/
|
||||||
|
@ -46,6 +46,7 @@ import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo025
|
|||||||
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo026
|
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo026
|
||||||
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo027
|
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo027
|
||||||
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo028
|
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo028
|
||||||
|
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo029
|
||||||
import org.matrix.android.sdk.internal.util.Normalizer
|
import org.matrix.android.sdk.internal.util.Normalizer
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@ -60,7 +61,7 @@ internal class RealmSessionStoreMigration @Inject constructor(
|
|||||||
override fun equals(other: Any?) = other is RealmSessionStoreMigration
|
override fun equals(other: Any?) = other is RealmSessionStoreMigration
|
||||||
override fun hashCode() = 1000
|
override fun hashCode() = 1000
|
||||||
|
|
||||||
val schemaVersion = 28L
|
val schemaVersion = 29L
|
||||||
|
|
||||||
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
|
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
|
||||||
Timber.d("Migrating Realm Session from $oldVersion to $newVersion")
|
Timber.d("Migrating Realm Session from $oldVersion to $newVersion")
|
||||||
@ -93,5 +94,6 @@ internal class RealmSessionStoreMigration @Inject constructor(
|
|||||||
if (oldVersion < 26) MigrateSessionTo026(realm).perform()
|
if (oldVersion < 26) MigrateSessionTo026(realm).perform()
|
||||||
if (oldVersion < 27) MigrateSessionTo027(realm).perform()
|
if (oldVersion < 27) MigrateSessionTo027(realm).perform()
|
||||||
if (oldVersion < 28) MigrateSessionTo028(realm).perform()
|
if (oldVersion < 28) MigrateSessionTo028(realm).perform()
|
||||||
|
if (oldVersion < 29) MigrateSessionTo029(realm).perform()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -58,7 +58,7 @@ internal object EventAnnotationsSummaryMapper {
|
|||||||
PollResponseAggregatedSummaryEntityMapper.map(it)
|
PollResponseAggregatedSummaryEntityMapper.map(it)
|
||||||
},
|
},
|
||||||
liveLocationShareAggregatedSummary = annotationsSummary.liveLocationShareAggregatedSummary?.let {
|
liveLocationShareAggregatedSummary = annotationsSummary.liveLocationShareAggregatedSummary?.let {
|
||||||
LiveLocationShareAggregatedSummaryMapper.map(it)
|
LiveLocationShareAggregatedSummaryMapper().map(it)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -20,11 +20,13 @@ import org.matrix.android.sdk.api.session.events.model.toModel
|
|||||||
import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationShareAggregatedSummary
|
import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationShareAggregatedSummary
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconLocationDataContent
|
import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconLocationDataContent
|
||||||
import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity
|
import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
internal object LiveLocationShareAggregatedSummaryMapper {
|
internal class LiveLocationShareAggregatedSummaryMapper @Inject constructor() {
|
||||||
|
|
||||||
fun map(entity: LiveLocationShareAggregatedSummaryEntity): LiveLocationShareAggregatedSummary {
|
fun map(entity: LiveLocationShareAggregatedSummaryEntity): LiveLocationShareAggregatedSummary {
|
||||||
return LiveLocationShareAggregatedSummary(
|
return LiveLocationShareAggregatedSummary(
|
||||||
|
userId = entity.userId,
|
||||||
isActive = entity.isActive,
|
isActive = entity.isActive,
|
||||||
endOfLiveTimestampMillis = entity.endOfLiveTimestampMillis,
|
endOfLiveTimestampMillis = entity.endOfLiveTimestampMillis,
|
||||||
lastLocationDataContent = ContentMapper.map(entity.lastLocationContent).toModel<MessageBeaconLocationDataContent>()
|
lastLocationDataContent = ContentMapper.map(entity.lastLocationContent).toModel<MessageBeaconLocationDataContent>()
|
||||||
|
@ -0,0 +1,37 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* 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 org.matrix.android.sdk.internal.database.migration
|
||||||
|
|
||||||
|
import io.realm.DynamicRealm
|
||||||
|
import io.realm.FieldAttribute
|
||||||
|
import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntityFields
|
||||||
|
import org.matrix.android.sdk.internal.util.database.RealmMigrator
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migrating to:
|
||||||
|
* Live location sharing aggregated summary: adding new field userId.
|
||||||
|
*/
|
||||||
|
internal class MigrateSessionTo029(realm: DynamicRealm) : RealmMigrator(realm, 28) {
|
||||||
|
|
||||||
|
override fun doMigrate(realm: DynamicRealm) {
|
||||||
|
realm.schema.get("LiveLocationShareAggregatedSummaryEntity")
|
||||||
|
?.addField(LiveLocationShareAggregatedSummaryEntityFields.USER_ID, String::class.java, FieldAttribute.REQUIRED)
|
||||||
|
?.transform { obj ->
|
||||||
|
obj.setString(LiveLocationShareAggregatedSummaryEntityFields.USER_ID, "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -31,6 +31,8 @@ internal open class LiveLocationShareAggregatedSummaryEntity(
|
|||||||
|
|
||||||
var roomId: String = "",
|
var roomId: String = "",
|
||||||
|
|
||||||
|
var userId: String = "",
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Indicate whether the live is currently running.
|
* Indicate whether the live is currently running.
|
||||||
*/
|
*/
|
||||||
|
@ -28,9 +28,15 @@ internal fun LiveLocationShareAggregatedSummaryEntity.Companion.where(
|
|||||||
roomId: String,
|
roomId: String,
|
||||||
eventId: String,
|
eventId: String,
|
||||||
): RealmQuery<LiveLocationShareAggregatedSummaryEntity> {
|
): RealmQuery<LiveLocationShareAggregatedSummaryEntity> {
|
||||||
|
return LiveLocationShareAggregatedSummaryEntity
|
||||||
|
.whereRoomId(realm, roomId = roomId)
|
||||||
|
.equalTo(LiveLocationShareAggregatedSummaryEntityFields.EVENT_ID, eventId)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun LiveLocationShareAggregatedSummaryEntity.Companion.whereRoomId(realm: Realm,
|
||||||
|
roomId: String): RealmQuery<LiveLocationShareAggregatedSummaryEntity> {
|
||||||
return realm.where<LiveLocationShareAggregatedSummaryEntity>()
|
return realm.where<LiveLocationShareAggregatedSummaryEntity>()
|
||||||
.equalTo(LiveLocationShareAggregatedSummaryEntityFields.ROOM_ID, roomId)
|
.equalTo(LiveLocationShareAggregatedSummaryEntityFields.ROOM_ID, roomId)
|
||||||
.equalTo(LiveLocationShareAggregatedSummaryEntityFields.EVENT_ID, eventId)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun LiveLocationShareAggregatedSummaryEntity.Companion.create(
|
internal fun LiveLocationShareAggregatedSummaryEntity.Companion.create(
|
||||||
@ -63,3 +69,31 @@ internal fun LiveLocationShareAggregatedSummaryEntity.Companion.get(
|
|||||||
): LiveLocationShareAggregatedSummaryEntity? {
|
): LiveLocationShareAggregatedSummaryEntity? {
|
||||||
return LiveLocationShareAggregatedSummaryEntity.where(realm, roomId, eventId).findFirst()
|
return LiveLocationShareAggregatedSummaryEntity.where(realm, roomId, eventId).findFirst()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal fun LiveLocationShareAggregatedSummaryEntity.Companion.findActiveLiveInRoomForUser(
|
||||||
|
realm: Realm,
|
||||||
|
roomId: String,
|
||||||
|
userId: String,
|
||||||
|
ignoredEventId: String
|
||||||
|
): List<LiveLocationShareAggregatedSummaryEntity> {
|
||||||
|
return LiveLocationShareAggregatedSummaryEntity
|
||||||
|
.whereRoomId(realm, roomId = roomId)
|
||||||
|
.equalTo(LiveLocationShareAggregatedSummaryEntityFields.USER_ID, userId)
|
||||||
|
.equalTo(LiveLocationShareAggregatedSummaryEntityFields.IS_ACTIVE, true)
|
||||||
|
.notEqualTo(LiveLocationShareAggregatedSummaryEntityFields.EVENT_ID, ignoredEventId)
|
||||||
|
.findAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A live is considered as running when active and with at least a last known location.
|
||||||
|
*/
|
||||||
|
internal fun LiveLocationShareAggregatedSummaryEntity.Companion.findRunningLiveInRoom(
|
||||||
|
realm: Realm,
|
||||||
|
roomId: String,
|
||||||
|
): RealmQuery<LiveLocationShareAggregatedSummaryEntity> {
|
||||||
|
return LiveLocationShareAggregatedSummaryEntity
|
||||||
|
.whereRoomId(realm, roomId = roomId)
|
||||||
|
.equalTo(LiveLocationShareAggregatedSummaryEntityFields.IS_ACTIVE, true)
|
||||||
|
.isNotEmpty(LiveLocationShareAggregatedSummaryEntityFields.USER_ID)
|
||||||
|
.isNotNull(LiveLocationShareAggregatedSummaryEntityFields.LAST_LOCATION_CONTENT)
|
||||||
|
}
|
||||||
|
@ -23,6 +23,7 @@ import org.matrix.android.sdk.api.session.room.accountdata.RoomAccountDataServic
|
|||||||
import org.matrix.android.sdk.api.session.room.alias.AliasService
|
import org.matrix.android.sdk.api.session.room.alias.AliasService
|
||||||
import org.matrix.android.sdk.api.session.room.call.RoomCallService
|
import org.matrix.android.sdk.api.session.room.call.RoomCallService
|
||||||
import org.matrix.android.sdk.api.session.room.crypto.RoomCryptoService
|
import org.matrix.android.sdk.api.session.room.crypto.RoomCryptoService
|
||||||
|
import org.matrix.android.sdk.api.session.room.location.LocationSharingService
|
||||||
import org.matrix.android.sdk.api.session.room.members.MembershipService
|
import org.matrix.android.sdk.api.session.room.members.MembershipService
|
||||||
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
||||||
import org.matrix.android.sdk.api.session.room.model.RoomType
|
import org.matrix.android.sdk.api.session.room.model.RoomType
|
||||||
@ -69,6 +70,7 @@ internal class DefaultRoom(
|
|||||||
private val roomAccountDataService: RoomAccountDataService,
|
private val roomAccountDataService: RoomAccountDataService,
|
||||||
private val roomVersionService: RoomVersionService,
|
private val roomVersionService: RoomVersionService,
|
||||||
private val viaParameterFinder: ViaParameterFinder,
|
private val viaParameterFinder: ViaParameterFinder,
|
||||||
|
private val locationSharingService: LocationSharingService,
|
||||||
override val coroutineDispatchers: MatrixCoroutineDispatchers
|
override val coroutineDispatchers: MatrixCoroutineDispatchers
|
||||||
) : Room {
|
) : Room {
|
||||||
|
|
||||||
@ -104,4 +106,5 @@ internal class DefaultRoom(
|
|||||||
override fun roomPushRuleService() = roomPushRuleService
|
override fun roomPushRuleService() = roomPushRuleService
|
||||||
override fun roomAccountDataService() = roomAccountDataService
|
override fun roomAccountDataService() = roomAccountDataService
|
||||||
override fun roomVersionService() = roomVersionService
|
override fun roomVersionService() = roomVersionService
|
||||||
|
override fun locationSharingService() = locationSharingService
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,7 @@ import org.matrix.android.sdk.internal.session.room.alias.DefaultAliasService
|
|||||||
import org.matrix.android.sdk.internal.session.room.call.DefaultRoomCallService
|
import org.matrix.android.sdk.internal.session.room.call.DefaultRoomCallService
|
||||||
import org.matrix.android.sdk.internal.session.room.crypto.DefaultRoomCryptoService
|
import org.matrix.android.sdk.internal.session.room.crypto.DefaultRoomCryptoService
|
||||||
import org.matrix.android.sdk.internal.session.room.draft.DefaultDraftService
|
import org.matrix.android.sdk.internal.session.room.draft.DefaultDraftService
|
||||||
|
import org.matrix.android.sdk.internal.session.room.location.DefaultLocationSharingService
|
||||||
import org.matrix.android.sdk.internal.session.room.membership.DefaultMembershipService
|
import org.matrix.android.sdk.internal.session.room.membership.DefaultMembershipService
|
||||||
import org.matrix.android.sdk.internal.session.room.notification.DefaultRoomPushRuleService
|
import org.matrix.android.sdk.internal.session.room.notification.DefaultRoomPushRuleService
|
||||||
import org.matrix.android.sdk.internal.session.room.read.DefaultReadService
|
import org.matrix.android.sdk.internal.session.room.read.DefaultReadService
|
||||||
@ -69,6 +70,7 @@ internal class DefaultRoomFactory @Inject constructor(
|
|||||||
private val roomVersionServiceFactory: DefaultRoomVersionService.Factory,
|
private val roomVersionServiceFactory: DefaultRoomVersionService.Factory,
|
||||||
private val roomAccountDataServiceFactory: DefaultRoomAccountDataService.Factory,
|
private val roomAccountDataServiceFactory: DefaultRoomAccountDataService.Factory,
|
||||||
private val viaParameterFinder: ViaParameterFinder,
|
private val viaParameterFinder: ViaParameterFinder,
|
||||||
|
private val locationSharingServiceFactory: DefaultLocationSharingService.Factory,
|
||||||
private val coroutineDispatchers: MatrixCoroutineDispatchers
|
private val coroutineDispatchers: MatrixCoroutineDispatchers
|
||||||
) : RoomFactory {
|
) : RoomFactory {
|
||||||
|
|
||||||
@ -96,6 +98,7 @@ internal class DefaultRoomFactory @Inject constructor(
|
|||||||
roomAccountDataService = roomAccountDataServiceFactory.create(roomId),
|
roomAccountDataService = roomAccountDataServiceFactory.create(roomId),
|
||||||
roomVersionService = roomVersionServiceFactory.create(roomId),
|
roomVersionService = roomVersionServiceFactory.create(roomId),
|
||||||
viaParameterFinder = viaParameterFinder,
|
viaParameterFinder = viaParameterFinder,
|
||||||
|
locationSharingService = locationSharingServiceFactory.create(roomId),
|
||||||
coroutineDispatchers = coroutineDispatchers
|
coroutineDispatchers = coroutineDispatchers
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -26,6 +26,7 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconInfoCo
|
|||||||
import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconLocationDataContent
|
import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconLocationDataContent
|
||||||
import org.matrix.android.sdk.internal.database.mapper.ContentMapper
|
import org.matrix.android.sdk.internal.database.mapper.ContentMapper
|
||||||
import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity
|
import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity
|
||||||
|
import org.matrix.android.sdk.internal.database.query.findActiveLiveInRoomForUser
|
||||||
import org.matrix.android.sdk.internal.database.query.getOrCreate
|
import org.matrix.android.sdk.internal.database.query.getOrCreate
|
||||||
import org.matrix.android.sdk.internal.di.SessionId
|
import org.matrix.android.sdk.internal.di.SessionId
|
||||||
import org.matrix.android.sdk.internal.di.WorkManagerProvider
|
import org.matrix.android.sdk.internal.di.WorkManagerProvider
|
||||||
@ -35,6 +36,7 @@ import timber.log.Timber
|
|||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
// TODO add unit tests
|
||||||
internal class LiveLocationAggregationProcessor @Inject constructor(
|
internal class LiveLocationAggregationProcessor @Inject constructor(
|
||||||
@SessionId private val sessionId: String,
|
@SessionId private val sessionId: String,
|
||||||
private val workManagerProvider: WorkManagerProvider,
|
private val workManagerProvider: WorkManagerProvider,
|
||||||
@ -70,6 +72,9 @@ internal class LiveLocationAggregationProcessor @Inject constructor(
|
|||||||
val endOfLiveTimestampMillis = content.getBestTimestampMillis()?.let { it + (content.timeout ?: 0) }
|
val endOfLiveTimestampMillis = content.getBestTimestampMillis()?.let { it + (content.timeout ?: 0) }
|
||||||
aggregatedSummary.endOfLiveTimestampMillis = endOfLiveTimestampMillis
|
aggregatedSummary.endOfLiveTimestampMillis = endOfLiveTimestampMillis
|
||||||
aggregatedSummary.isActive = isLive
|
aggregatedSummary.isActive = isLive
|
||||||
|
aggregatedSummary.userId = event.senderId
|
||||||
|
|
||||||
|
deactivateAllPreviousBeacons(realm, roomId, event.senderId, targetEventId)
|
||||||
|
|
||||||
if (isLive) {
|
if (isLive) {
|
||||||
scheduleDeactivationAfterTimeout(targetEventId, roomId, endOfLiveTimestampMillis)
|
scheduleDeactivationAfterTimeout(targetEventId, roomId, endOfLiveTimestampMillis)
|
||||||
@ -137,5 +142,16 @@ internal class LiveLocationAggregationProcessor @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun deactivateAllPreviousBeacons(realm: Realm, roomId: String, userId: String, currentEventId: String) {
|
||||||
|
LiveLocationShareAggregatedSummaryEntity
|
||||||
|
.findActiveLiveInRoomForUser(
|
||||||
|
realm = realm,
|
||||||
|
roomId = roomId,
|
||||||
|
userId = userId,
|
||||||
|
ignoredEventId = currentEventId
|
||||||
|
)
|
||||||
|
.forEach { it.isActive = false }
|
||||||
|
}
|
||||||
|
|
||||||
private fun Long.isMoreRecentThan(timestamp: Long) = this > timestamp
|
private fun Long.isMoreRecentThan(timestamp: Long) = this > timestamp
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,49 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* 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 org.matrix.android.sdk.internal.session.room.location
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import com.zhuinden.monarchy.Monarchy
|
||||||
|
import dagger.assisted.Assisted
|
||||||
|
import dagger.assisted.AssistedFactory
|
||||||
|
import dagger.assisted.AssistedInject
|
||||||
|
import org.matrix.android.sdk.api.session.room.location.LocationSharingService
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationShareAggregatedSummary
|
||||||
|
import org.matrix.android.sdk.internal.database.mapper.LiveLocationShareAggregatedSummaryMapper
|
||||||
|
import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity
|
||||||
|
import org.matrix.android.sdk.internal.database.query.findRunningLiveInRoom
|
||||||
|
import org.matrix.android.sdk.internal.di.SessionDatabase
|
||||||
|
|
||||||
|
// TODO add unit tests
|
||||||
|
internal class DefaultLocationSharingService @AssistedInject constructor(
|
||||||
|
@Assisted private val roomId: String,
|
||||||
|
@SessionDatabase private val monarchy: Monarchy,
|
||||||
|
private val liveLocationShareAggregatedSummaryMapper: LiveLocationShareAggregatedSummaryMapper,
|
||||||
|
) : LocationSharingService {
|
||||||
|
|
||||||
|
@AssistedFactory
|
||||||
|
interface Factory {
|
||||||
|
fun create(roomId: String): DefaultLocationSharingService
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getRunningLiveLocationShareSummaries(): LiveData<List<LiveLocationShareAggregatedSummary>> {
|
||||||
|
return monarchy.findAllMappedWithChanges(
|
||||||
|
{ LiveLocationShareAggregatedSummaryEntity.findRunningLiveInRoom(it, roomId = roomId) },
|
||||||
|
{ liveLocationShareAggregatedSummaryMapper.map(it) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,56 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* 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 org.matrix.android.sdk.internal.database.mapper
|
||||||
|
|
||||||
|
import com.squareup.moshi.Moshi
|
||||||
|
import org.amshove.kluent.shouldBeEqualTo
|
||||||
|
import org.junit.Test
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationShareAggregatedSummary
|
||||||
|
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_USER_ID = "a-user-id"
|
||||||
|
private const val ANY_ACTIVE_STATE = true
|
||||||
|
private const val ANY_TIMEOUT = 123L
|
||||||
|
private val A_LOCATION_INFO = LocationInfo("a-geo-uri")
|
||||||
|
|
||||||
|
class LiveLocationShareAggregatedSummaryMapperTest {
|
||||||
|
|
||||||
|
private val mapper = LiveLocationShareAggregatedSummaryMapper()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given an entity then result should be mapped correctly`() {
|
||||||
|
val entity = anEntity(content = MessageBeaconLocationDataContent(locationInfo = A_LOCATION_INFO))
|
||||||
|
|
||||||
|
val summary = mapper.map(entity)
|
||||||
|
|
||||||
|
summary shouldBeEqualTo LiveLocationShareAggregatedSummary(
|
||||||
|
userId = ANY_USER_ID,
|
||||||
|
isActive = ANY_ACTIVE_STATE,
|
||||||
|
endOfLiveTimestampMillis = ANY_TIMEOUT,
|
||||||
|
lastLocationDataContent = MessageBeaconLocationDataContent(locationInfo = A_LOCATION_INFO)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun anEntity(content: MessageBeaconLocationDataContent) = LiveLocationShareAggregatedSummaryEntity(
|
||||||
|
userId = ANY_USER_ID,
|
||||||
|
isActive = ANY_ACTIVE_STATE,
|
||||||
|
endOfLiveTimestampMillis = ANY_TIMEOUT,
|
||||||
|
lastLocationContent = Moshi.Builder().build().adapter(MessageBeaconLocationDataContent::class.java).toJson(content)
|
||||||
|
)
|
||||||
|
}
|
@ -54,6 +54,7 @@ import im.vector.app.features.home.room.list.RoomListViewModel
|
|||||||
import im.vector.app.features.homeserver.HomeServerCapabilitiesViewModel
|
import im.vector.app.features.homeserver.HomeServerCapabilitiesViewModel
|
||||||
import im.vector.app.features.invite.InviteUsersToRoomViewModel
|
import im.vector.app.features.invite.InviteUsersToRoomViewModel
|
||||||
import im.vector.app.features.location.LocationSharingViewModel
|
import im.vector.app.features.location.LocationSharingViewModel
|
||||||
|
import im.vector.app.features.location.live.map.LocationLiveMapViewModel
|
||||||
import im.vector.app.features.login.LoginViewModel
|
import im.vector.app.features.login.LoginViewModel
|
||||||
import im.vector.app.features.login2.LoginViewModel2
|
import im.vector.app.features.login2.LoginViewModel2
|
||||||
import im.vector.app.features.login2.created.AccountCreatedViewModel
|
import im.vector.app.features.login2.created.AccountCreatedViewModel
|
||||||
@ -600,4 +601,9 @@ interface MavericksViewModelModule {
|
|||||||
@IntoMap
|
@IntoMap
|
||||||
@MavericksViewModelKey(VectorAttachmentViewerViewModel::class)
|
@MavericksViewModelKey(VectorAttachmentViewerViewModel::class)
|
||||||
fun vectorAttachmentViewerViewModelFactory(factory: VectorAttachmentViewerViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
|
fun vectorAttachmentViewerViewModelFactory(factory: VectorAttachmentViewerViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@IntoMap
|
||||||
|
@MavericksViewModelKey(LocationLiveMapViewModel::class)
|
||||||
|
fun locationLiveMapViewModelFactory(factory: LocationLiveMapViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
|
||||||
}
|
}
|
||||||
|
@ -181,7 +181,7 @@ class LocationSharingFragment @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun handleZoomToUserLocationEvent(event: LocationSharingViewEvents.ZoomToUserLocation) {
|
private fun handleZoomToUserLocationEvent(event: LocationSharingViewEvents.ZoomToUserLocation) {
|
||||||
views.mapView.zoomToLocation(event.userLocation.latitude, event.userLocation.longitude)
|
views.mapView.zoomToLocation(event.userLocation)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleStartLiveLocationService(event: LocationSharingViewEvents.StartLiveLocationService) {
|
private fun handleStartLiveLocationService(event: LocationSharingViewEvents.StartLiveLocationService) {
|
||||||
|
@ -0,0 +1,39 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2022 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 im.vector.app.features.location
|
||||||
|
|
||||||
|
import com.mapbox.mapboxsdk.camera.CameraPosition
|
||||||
|
import com.mapbox.mapboxsdk.constants.MapboxConstants
|
||||||
|
import com.mapbox.mapboxsdk.geometry.LatLng
|
||||||
|
import com.mapbox.mapboxsdk.geometry.LatLngBounds
|
||||||
|
import com.mapbox.mapboxsdk.maps.MapboxMap
|
||||||
|
|
||||||
|
fun MapboxMap?.zoomToLocation(locationData: LocationData) {
|
||||||
|
this?.cameraPosition = CameraPosition.Builder()
|
||||||
|
.target(LatLng(locationData.latitude, locationData.longitude))
|
||||||
|
.zoom(INITIAL_MAP_ZOOM_IN_PREVIEW)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun MapboxMap?.zoomToBounds(latLngBounds: LatLngBounds) {
|
||||||
|
this?.getCameraForLatLngBounds(latLngBounds)?.let { camPosition ->
|
||||||
|
// unZoom a little to avoid having pins exactly at the edges of the map
|
||||||
|
cameraPosition = CameraPosition.Builder(camPosition)
|
||||||
|
.zoom((camPosition.zoom - 1).coerceAtLeast(MapboxConstants.MINIMUM_ZOOM.toDouble()))
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
@ -25,7 +25,6 @@ import androidx.core.content.ContextCompat
|
|||||||
import androidx.core.view.marginBottom
|
import androidx.core.view.marginBottom
|
||||||
import androidx.core.view.marginTop
|
import androidx.core.view.marginTop
|
||||||
import androidx.core.view.updateLayoutParams
|
import androidx.core.view.updateLayoutParams
|
||||||
import com.mapbox.mapboxsdk.camera.CameraPosition
|
|
||||||
import com.mapbox.mapboxsdk.geometry.LatLng
|
import com.mapbox.mapboxsdk.geometry.LatLng
|
||||||
import com.mapbox.mapboxsdk.maps.MapView
|
import com.mapbox.mapboxsdk.maps.MapView
|
||||||
import com.mapbox.mapboxsdk.maps.MapboxMap
|
import com.mapbox.mapboxsdk.maps.MapboxMap
|
||||||
@ -164,7 +163,7 @@ class MapTilerMapView @JvmOverloads constructor(
|
|||||||
|
|
||||||
state.userLocationData?.let { locationData ->
|
state.userLocationData?.let { locationData ->
|
||||||
if (!initZoomDone || !state.zoomOnlyOnce) {
|
if (!initZoomDone || !state.zoomOnlyOnce) {
|
||||||
zoomToLocation(locationData.latitude, locationData.longitude)
|
zoomToLocation(locationData)
|
||||||
initZoomDone = true
|
initZoomDone = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -180,12 +179,9 @@ class MapTilerMapView @JvmOverloads constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun zoomToLocation(latitude: Double, longitude: Double) {
|
fun zoomToLocation(locationData: LocationData) {
|
||||||
Timber.d("## Location: zoomToLocation")
|
Timber.d("## Location: zoomToLocation")
|
||||||
mapRefs?.map?.cameraPosition = CameraPosition.Builder()
|
mapRefs?.map?.zoomToLocation(locationData)
|
||||||
.target(LatLng(latitude, longitude))
|
|
||||||
.zoom(INITIAL_MAP_ZOOM_IN_PREVIEW)
|
|
||||||
.build()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getLocationOfMapCenter(): LocationData? =
|
fun getLocationOfMapCenter(): LocationData? =
|
||||||
|
@ -0,0 +1,40 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2022 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 im.vector.app.features.location.live.map
|
||||||
|
|
||||||
|
import androidx.lifecycle.asFlow
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.emptyFlow
|
||||||
|
import kotlinx.coroutines.flow.mapLatest
|
||||||
|
import org.matrix.android.sdk.api.session.Session
|
||||||
|
import org.matrix.android.sdk.api.session.getRoom
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class GetListOfUserLiveLocationUseCase @Inject constructor(
|
||||||
|
private val session: Session,
|
||||||
|
private val userLiveLocationViewStateMapper: UserLiveLocationViewStateMapper,
|
||||||
|
) {
|
||||||
|
|
||||||
|
fun execute(roomId: String): Flow<List<UserLiveLocationViewState>> {
|
||||||
|
return session.getRoom(roomId)
|
||||||
|
?.locationSharingService()
|
||||||
|
?.getRunningLiveLocationShareSummaries()
|
||||||
|
?.asFlow()
|
||||||
|
?.mapLatest { it.mapNotNull { summary -> userLiveLocationViewStateMapper.map(summary) } }
|
||||||
|
?: emptyFlow()
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,24 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2022 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 im.vector.app.features.location.live.map
|
||||||
|
|
||||||
|
import im.vector.app.core.platform.VectorViewModelAction
|
||||||
|
|
||||||
|
sealed class LocationLiveMapAction : VectorViewModelAction {
|
||||||
|
data class AddMapSymbol(val key: String, val value: Long) : LocationLiveMapAction()
|
||||||
|
data class RemoveMapSymbol(val key: String) : LocationLiveMapAction()
|
||||||
|
}
|
@ -0,0 +1,21 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2022 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 im.vector.app.features.location.live.map
|
||||||
|
|
||||||
|
import im.vector.app.core.platform.VectorViewEvents
|
||||||
|
|
||||||
|
sealed interface LocationLiveMapViewEvents : VectorViewEvents
|
@ -16,48 +16,73 @@
|
|||||||
|
|
||||||
package im.vector.app.features.location.live.map
|
package im.vector.app.features.location.live.map
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.graphics.drawable.Drawable
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import androidx.core.graphics.drawable.toBitmap
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.airbnb.mvrx.args
|
import com.airbnb.mvrx.fragmentViewModel
|
||||||
|
import com.airbnb.mvrx.withState
|
||||||
|
import com.mapbox.mapboxsdk.geometry.LatLng
|
||||||
|
import com.mapbox.mapboxsdk.geometry.LatLngBounds
|
||||||
|
import com.mapbox.mapboxsdk.maps.MapView
|
||||||
|
import com.mapbox.mapboxsdk.maps.MapboxMap
|
||||||
import com.mapbox.mapboxsdk.maps.MapboxMapOptions
|
import com.mapbox.mapboxsdk.maps.MapboxMapOptions
|
||||||
|
import com.mapbox.mapboxsdk.maps.Style
|
||||||
import com.mapbox.mapboxsdk.maps.SupportMapFragment
|
import com.mapbox.mapboxsdk.maps.SupportMapFragment
|
||||||
|
import com.mapbox.mapboxsdk.plugins.annotation.SymbolManager
|
||||||
|
import com.mapbox.mapboxsdk.plugins.annotation.SymbolOptions
|
||||||
|
import com.mapbox.mapboxsdk.style.layers.Property
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.core.extensions.addChildFragment
|
import im.vector.app.core.extensions.addChildFragment
|
||||||
import im.vector.app.core.platform.VectorBaseFragment
|
import im.vector.app.core.platform.VectorBaseFragment
|
||||||
import im.vector.app.databinding.FragmentSimpleContainerBinding
|
import im.vector.app.databinding.FragmentSimpleContainerBinding
|
||||||
import im.vector.app.features.location.UrlMapProvider
|
import im.vector.app.features.location.UrlMapProvider
|
||||||
|
import im.vector.app.features.location.zoomToBounds
|
||||||
|
import im.vector.app.features.location.zoomToLocation
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.lang.ref.WeakReference
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Screen showing a map with all the current users sharing their live location in room.
|
* Screen showing a map with all the current users sharing their live location in a room.
|
||||||
*/
|
*/
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class LocationLiveMapViewFragment : VectorBaseFragment<FragmentSimpleContainerBinding>() {
|
class LocationLiveMapViewFragment @Inject constructor() : VectorBaseFragment<FragmentSimpleContainerBinding>() {
|
||||||
|
|
||||||
@Inject
|
@Inject lateinit var urlMapProvider: UrlMapProvider
|
||||||
lateinit var urlMapProvider: UrlMapProvider
|
|
||||||
|
|
||||||
private val args: LocationLiveMapViewArgs by args()
|
private val viewModel: LocationLiveMapViewModel by fragmentViewModel()
|
||||||
|
|
||||||
|
private var mapboxMap: WeakReference<MapboxMap>? = null
|
||||||
|
private var symbolManager: SymbolManager? = null
|
||||||
|
private var mapStyle: Style? = null
|
||||||
|
private val pendingLiveLocations = mutableListOf<UserLiveLocationViewState>()
|
||||||
|
private var isMapFirstUpdate = true
|
||||||
|
|
||||||
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentSimpleContainerBinding {
|
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentSimpleContainerBinding {
|
||||||
return FragmentSimpleContainerBinding.inflate(layoutInflater, container, false)
|
return FragmentSimpleContainerBinding.inflate(layoutInflater, container, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onResume() {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onResume()
|
||||||
setupMap()
|
setupMap()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupMap() {
|
private fun setupMap() {
|
||||||
val mapFragment = getOrCreateSupportMapFragment()
|
val mapFragment = getOrCreateSupportMapFragment()
|
||||||
|
mapFragment.getMapAsync { mapboxMap ->
|
||||||
mapFragment.getMapAsync { mapBoxMap ->
|
lifecycleScope.launch {
|
||||||
lifecycleScope.launchWhenCreated {
|
mapboxMap.setStyle(urlMapProvider.getMapUrl()) { style ->
|
||||||
mapBoxMap.setStyle(urlMapProvider.getMapUrl())
|
mapStyle = style
|
||||||
|
this@LocationLiveMapViewFragment.mapboxMap = WeakReference(mapboxMap)
|
||||||
|
symbolManager = SymbolManager(mapFragment.view as MapView, mapboxMap, style)
|
||||||
|
pendingLiveLocations
|
||||||
|
.takeUnless { it.isEmpty() }
|
||||||
|
?.let { updateMap(it) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -70,6 +95,101 @@ class LocationLiveMapViewFragment : VectorBaseFragment<FragmentSimpleContainerBi
|
|||||||
.also { addChildFragment(R.id.fragmentContainer, it, tag = MAP_FRAGMENT_TAG) }
|
.also { addChildFragment(R.id.fragmentContainer, it, tag = MAP_FRAGMENT_TAG) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun invalidate() = withState(viewModel) { viewState ->
|
||||||
|
updateMap(viewState.userLocations)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateMap(userLiveLocations: List<UserLiveLocationViewState>) {
|
||||||
|
symbolManager?.let { sManager ->
|
||||||
|
val latLngBoundsBuilder = LatLngBounds.Builder()
|
||||||
|
userLiveLocations.forEach { userLocation ->
|
||||||
|
createOrUpdateSymbol(userLocation, sManager)
|
||||||
|
if (isMapFirstUpdate) {
|
||||||
|
val latLng = LatLng(userLocation.locationData.latitude, userLocation.locationData.longitude)
|
||||||
|
latLngBoundsBuilder.include(latLng)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
removeOutdatedSymbols(userLiveLocations, sManager)
|
||||||
|
updateMapZoomWhenNeeded(userLiveLocations, latLngBoundsBuilder)
|
||||||
|
} ?: postponeUpdateOfMap(userLiveLocations)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createOrUpdateSymbol(userLocation: UserLiveLocationViewState, symbolManager: SymbolManager) = withState(viewModel) { state ->
|
||||||
|
val symbolId = state.mapSymbolIds[userLocation.userId]
|
||||||
|
|
||||||
|
if (symbolId == null || symbolManager.annotations.get(symbolId) == null) {
|
||||||
|
createSymbol(userLocation, symbolManager)
|
||||||
|
} else {
|
||||||
|
updateSymbol(symbolId, userLocation, symbolManager)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createSymbol(userLocation: UserLiveLocationViewState, symbolManager: SymbolManager) {
|
||||||
|
addUserPinToMapStyle(userLocation.userId, userLocation.pinDrawable)
|
||||||
|
val symbolOptions = buildSymbolOptions(userLocation)
|
||||||
|
val symbol = symbolManager.create(symbolOptions)
|
||||||
|
viewModel.handle(LocationLiveMapAction.AddMapSymbol(userLocation.userId, symbol.id))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateSymbol(symbolId: Long, userLocation: UserLiveLocationViewState, symbolManager: SymbolManager) {
|
||||||
|
val newLocation = LatLng(userLocation.locationData.latitude, userLocation.locationData.longitude)
|
||||||
|
val symbol = symbolManager.annotations.get(symbolId)
|
||||||
|
symbol?.let {
|
||||||
|
it.latLng = newLocation
|
||||||
|
symbolManager.update(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun removeOutdatedSymbols(userLiveLocations: List<UserLiveLocationViewState>, symbolManager: SymbolManager) = withState(viewModel) { state ->
|
||||||
|
val userIdsToRemove = state.mapSymbolIds.keys.subtract(userLiveLocations.map { it.userId }.toSet())
|
||||||
|
userIdsToRemove.forEach { userId ->
|
||||||
|
removeUserPinFromMapStyle(userId)
|
||||||
|
viewModel.handle(LocationLiveMapAction.RemoveMapSymbol(userId))
|
||||||
|
|
||||||
|
state.mapSymbolIds[userId]?.let { symbolId ->
|
||||||
|
Timber.d("trying to delete symbol with id: $symbolId")
|
||||||
|
symbolManager.annotations.get(symbolId)?.let {
|
||||||
|
symbolManager.delete(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateMapZoomWhenNeeded(userLiveLocations: List<UserLiveLocationViewState>, latLngBoundsBuilder: LatLngBounds.Builder) {
|
||||||
|
if (userLiveLocations.isNotEmpty() && isMapFirstUpdate) {
|
||||||
|
isMapFirstUpdate = false
|
||||||
|
if (userLiveLocations.size > 1) {
|
||||||
|
mapboxMap?.get()?.zoomToBounds(latLngBoundsBuilder.build())
|
||||||
|
} else {
|
||||||
|
mapboxMap?.get()?.zoomToLocation(userLiveLocations.first().locationData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun postponeUpdateOfMap(userLiveLocations: List<UserLiveLocationViewState>) {
|
||||||
|
pendingLiveLocations.clear()
|
||||||
|
pendingLiveLocations.addAll(userLiveLocations)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addUserPinToMapStyle(userId: String, userPinDrawable: Drawable) {
|
||||||
|
mapStyle?.let { style ->
|
||||||
|
if (style.getImage(userId) == null) {
|
||||||
|
style.addImage(userId, userPinDrawable.toBitmap())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun removeUserPinFromMapStyle(userId: String) {
|
||||||
|
mapStyle?.removeImage(userId)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildSymbolOptions(userLiveLocation: UserLiveLocationViewState) =
|
||||||
|
SymbolOptions()
|
||||||
|
.withLatLng(LatLng(userLiveLocation.locationData.latitude, userLiveLocation.locationData.longitude))
|
||||||
|
.withIconImage(userLiveLocation.userId)
|
||||||
|
.withIconAnchor(Property.ICON_ANCHOR_BOTTOM)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val MAP_FRAGMENT_TAG = "im.vector.app.features.location.live.map"
|
private const val MAP_FRAGMENT_TAG = "im.vector.app.features.location.live.map"
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,67 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2022 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 im.vector.app.features.location.live.map
|
||||||
|
|
||||||
|
import com.airbnb.mvrx.MavericksViewModelFactory
|
||||||
|
import dagger.assisted.Assisted
|
||||||
|
import dagger.assisted.AssistedFactory
|
||||||
|
import dagger.assisted.AssistedInject
|
||||||
|
import im.vector.app.core.di.MavericksAssistedViewModelFactory
|
||||||
|
import im.vector.app.core.di.hiltMavericksViewModelFactory
|
||||||
|
import im.vector.app.core.platform.VectorViewModel
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
|
||||||
|
class LocationLiveMapViewModel @AssistedInject constructor(
|
||||||
|
@Assisted private val initialState: LocationLiveMapViewState,
|
||||||
|
getListOfUserLiveLocationUseCase: GetListOfUserLiveLocationUseCase
|
||||||
|
) : VectorViewModel<LocationLiveMapViewState, LocationLiveMapAction, LocationLiveMapViewEvents>(initialState) {
|
||||||
|
|
||||||
|
@AssistedFactory
|
||||||
|
interface Factory : MavericksAssistedViewModelFactory<LocationLiveMapViewModel, LocationLiveMapViewState> {
|
||||||
|
override fun create(initialState: LocationLiveMapViewState): LocationLiveMapViewModel
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object : MavericksViewModelFactory<LocationLiveMapViewModel, LocationLiveMapViewState> by hiltMavericksViewModelFactory()
|
||||||
|
|
||||||
|
init {
|
||||||
|
getListOfUserLiveLocationUseCase.execute(initialState.roomId)
|
||||||
|
.onEach { setState { copy(userLocations = it) } }
|
||||||
|
.launchIn(viewModelScope)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handle(action: LocationLiveMapAction) {
|
||||||
|
when (action) {
|
||||||
|
is LocationLiveMapAction.AddMapSymbol -> handleAddMapSymbol(action)
|
||||||
|
is LocationLiveMapAction.RemoveMapSymbol -> handleRemoveMapSymbol(action)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleAddMapSymbol(action: LocationLiveMapAction.AddMapSymbol) = withState { state ->
|
||||||
|
val newMapSymbolIds = state.mapSymbolIds.toMutableMap().apply { set(action.key, action.value) }
|
||||||
|
setState {
|
||||||
|
copy(mapSymbolIds = newMapSymbolIds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleRemoveMapSymbol(action: LocationLiveMapAction.RemoveMapSymbol) = withState { state ->
|
||||||
|
val newMapSymbolIds = state.mapSymbolIds.toMutableMap().apply { remove(action.key) }
|
||||||
|
setState {
|
||||||
|
copy(mapSymbolIds = newMapSymbolIds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2022 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 im.vector.app.features.location.live.map
|
||||||
|
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import com.airbnb.mvrx.MavericksState
|
||||||
|
import im.vector.app.features.location.LocationData
|
||||||
|
|
||||||
|
data class LocationLiveMapViewState(
|
||||||
|
val roomId: String,
|
||||||
|
val userLocations: List<UserLiveLocationViewState> = emptyList(),
|
||||||
|
/**
|
||||||
|
* Map to keep track of symbol ids associated to each user Id.
|
||||||
|
*/
|
||||||
|
val mapSymbolIds: Map<String, Long> = emptyMap()
|
||||||
|
) : MavericksState {
|
||||||
|
constructor(locationLiveMapViewArgs: LocationLiveMapViewArgs) : this(
|
||||||
|
roomId = locationLiveMapViewArgs.roomId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class UserLiveLocationViewState(
|
||||||
|
val userId: String,
|
||||||
|
val pinDrawable: Drawable,
|
||||||
|
val locationData: LocationData,
|
||||||
|
val endOfLiveTimestampMillis: Long?
|
||||||
|
)
|
@ -0,0 +1,56 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2022 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 im.vector.app.features.location.live.map
|
||||||
|
|
||||||
|
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.room.model.livelocation.LiveLocationShareAggregatedSummary
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class UserLiveLocationViewStateMapper @Inject constructor(
|
||||||
|
private val locationPinProvider: LocationPinProvider,
|
||||||
|
) {
|
||||||
|
|
||||||
|
suspend fun map(liveLocationShareAggregatedSummary: LiveLocationShareAggregatedSummary) =
|
||||||
|
suspendCancellableCoroutine<UserLiveLocationViewState?> { continuation ->
|
||||||
|
val userId = liveLocationShareAggregatedSummary.userId
|
||||||
|
val locationData = liveLocationShareAggregatedSummary.lastLocationDataContent
|
||||||
|
?.getBestLocationInfo()
|
||||||
|
?.geoUri
|
||||||
|
.toLocationData()
|
||||||
|
|
||||||
|
when {
|
||||||
|
userId.isNullOrEmpty() || locationData == null -> continuation.resume(null) {
|
||||||
|
// do nothing on cancellation
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
locationPinProvider.create(userId) { pinDrawable ->
|
||||||
|
val viewState = UserLiveLocationViewState(
|
||||||
|
userId = userId,
|
||||||
|
pinDrawable = pinDrawable,
|
||||||
|
locationData = locationData,
|
||||||
|
endOfLiveTimestampMillis = liveLocationShareAggregatedSummary.endOfLiveTimestampMillis
|
||||||
|
)
|
||||||
|
continuation.resume(viewState) {
|
||||||
|
// do nothing on cancellation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,110 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2022 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 im.vector.app.features.location.live.map
|
||||||
|
|
||||||
|
import androidx.lifecycle.asFlow
|
||||||
|
import com.airbnb.mvrx.test.MvRxTestRule
|
||||||
|
import im.vector.app.features.location.LocationData
|
||||||
|
import im.vector.app.test.fakes.FakeSession
|
||||||
|
import io.mockk.coEvery
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.mockkStatic
|
||||||
|
import io.mockk.unmockkStatic
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.flow.flowOf
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.amshove.kluent.internal.assertEquals
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationShareAggregatedSummary
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconLocationDataContent
|
||||||
|
|
||||||
|
class GetListOfUserLiveLocationUseCaseTest {
|
||||||
|
|
||||||
|
@get:Rule
|
||||||
|
val mvRxTestRule = MvRxTestRule()
|
||||||
|
|
||||||
|
private val fakeSession = FakeSession()
|
||||||
|
|
||||||
|
private val viewStateMapper = mockk<UserLiveLocationViewStateMapper>()
|
||||||
|
|
||||||
|
private val getListOfUserLiveLocationUseCase = GetListOfUserLiveLocationUseCase(fakeSession, viewStateMapper)
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
mockkStatic("androidx.lifecycle.FlowLiveDataConversions")
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun tearDown() {
|
||||||
|
unmockkStatic("androidx.lifecycle.FlowLiveDataConversions")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given a room id then the correct flow of view states list is collected`() = runTest {
|
||||||
|
val roomId = "roomId"
|
||||||
|
|
||||||
|
val summary1 = LiveLocationShareAggregatedSummary(
|
||||||
|
userId = "userId1",
|
||||||
|
isActive = true,
|
||||||
|
endOfLiveTimestampMillis = 123,
|
||||||
|
lastLocationDataContent = MessageBeaconLocationDataContent()
|
||||||
|
)
|
||||||
|
val summary2 = LiveLocationShareAggregatedSummary(
|
||||||
|
userId = "userId2",
|
||||||
|
isActive = true,
|
||||||
|
endOfLiveTimestampMillis = 1234,
|
||||||
|
lastLocationDataContent = MessageBeaconLocationDataContent()
|
||||||
|
)
|
||||||
|
val summary3 = LiveLocationShareAggregatedSummary(
|
||||||
|
userId = "userId3",
|
||||||
|
isActive = true,
|
||||||
|
endOfLiveTimestampMillis = 1234,
|
||||||
|
lastLocationDataContent = MessageBeaconLocationDataContent()
|
||||||
|
)
|
||||||
|
val summaries = listOf(summary1, summary2, summary3)
|
||||||
|
val liveData = fakeSession.roomService()
|
||||||
|
.getRoom(roomId)
|
||||||
|
.locationSharingService()
|
||||||
|
.givenRunningLiveLocationShareSummaries(summaries)
|
||||||
|
|
||||||
|
every { liveData.asFlow() } returns flowOf(summaries)
|
||||||
|
|
||||||
|
val viewState1 = UserLiveLocationViewState(
|
||||||
|
userId = "userId1",
|
||||||
|
pinDrawable = mockk(),
|
||||||
|
locationData = LocationData(latitude = 1.0, longitude = 2.0, uncertainty = null),
|
||||||
|
endOfLiveTimestampMillis = 123
|
||||||
|
)
|
||||||
|
val viewState2 = UserLiveLocationViewState(
|
||||||
|
userId = "userId2",
|
||||||
|
pinDrawable = mockk(),
|
||||||
|
locationData = LocationData(latitude = 1.0, longitude = 2.0, uncertainty = null),
|
||||||
|
endOfLiveTimestampMillis = 1234
|
||||||
|
)
|
||||||
|
coEvery { viewStateMapper.map(summary1) } returns viewState1
|
||||||
|
coEvery { viewStateMapper.map(summary2) } returns viewState2
|
||||||
|
coEvery { viewStateMapper.map(summary3) } returns null
|
||||||
|
|
||||||
|
val viewStates = getListOfUserLiveLocationUseCase.execute(roomId).first()
|
||||||
|
|
||||||
|
assertEquals(listOf(viewState1, viewState2), viewStates)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,70 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2022 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 im.vector.app.features.location.live.map
|
||||||
|
|
||||||
|
import com.airbnb.mvrx.test.MvRxTestRule
|
||||||
|
import im.vector.app.features.location.LocationData
|
||||||
|
import im.vector.app.test.test
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import kotlinx.coroutines.flow.flowOf
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class LocationLiveMapViewModelTest {
|
||||||
|
|
||||||
|
@get:Rule
|
||||||
|
val mvrxTestRule = MvRxTestRule()
|
||||||
|
|
||||||
|
private val fakeRoomId = ""
|
||||||
|
|
||||||
|
private val args = LocationLiveMapViewArgs(roomId = fakeRoomId)
|
||||||
|
|
||||||
|
private val getListOfUserLiveLocationUseCase = mockk<GetListOfUserLiveLocationUseCase>()
|
||||||
|
|
||||||
|
private fun createViewModel(): LocationLiveMapViewModel {
|
||||||
|
return LocationLiveMapViewModel(
|
||||||
|
LocationLiveMapViewState(args),
|
||||||
|
getListOfUserLiveLocationUseCase
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given the viewModel has been initialized then viewState contains user locations list`() = runTest {
|
||||||
|
val userLocations = listOf(
|
||||||
|
UserLiveLocationViewState(
|
||||||
|
userId = "",
|
||||||
|
pinDrawable = mockk(),
|
||||||
|
locationData = LocationData(latitude = 1.0, longitude = 2.0, uncertainty = null),
|
||||||
|
endOfLiveTimestampMillis = 123
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
every { getListOfUserLiveLocationUseCase.execute(fakeRoomId) } returns flowOf(userLocations)
|
||||||
|
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
viewModel
|
||||||
|
.test()
|
||||||
|
.assertState(
|
||||||
|
LocationLiveMapViewState(args).copy(
|
||||||
|
userLocations = userLocations
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,93 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2022 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 im.vector.app.features.location.live.map
|
||||||
|
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import im.vector.app.features.location.LocationData
|
||||||
|
import im.vector.app.test.fakes.FakeLocationPinProvider
|
||||||
|
import io.mockk.mockk
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.amshove.kluent.shouldBeEqualTo
|
||||||
|
import org.junit.Test
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationShareAggregatedSummary
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.message.LocationInfo
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconLocationDataContent
|
||||||
|
|
||||||
|
private const val A_USER_ID = "aUserId"
|
||||||
|
private const val A_IS_ACTIVE = true
|
||||||
|
private const val A_END_OF_LIVE_TIMESTAMP = 123L
|
||||||
|
private const val A_LATITUDE = 40.05
|
||||||
|
private const val A_LONGITUDE = 29.24
|
||||||
|
private const val A_UNCERTAINTY = 30.0
|
||||||
|
private const val A_GEO_URI = "geo:$A_LATITUDE,$A_LONGITUDE;$A_UNCERTAINTY"
|
||||||
|
|
||||||
|
class UserLiveLocationViewStateMapperTest {
|
||||||
|
|
||||||
|
private val locationPinProvider = FakeLocationPinProvider()
|
||||||
|
|
||||||
|
private val userLiveLocationViewStateMapper = UserLiveLocationViewStateMapper(locationPinProvider.instance)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given a summary with invalid data then result is null`() = runTest {
|
||||||
|
val summary1 = LiveLocationShareAggregatedSummary(
|
||||||
|
userId = null,
|
||||||
|
isActive = true,
|
||||||
|
endOfLiveTimestampMillis = null,
|
||||||
|
lastLocationDataContent = null,
|
||||||
|
)
|
||||||
|
val summary2 = summary1.copy(userId = "")
|
||||||
|
val summaryWithoutLocation = summary1.copy(userId = A_USER_ID)
|
||||||
|
|
||||||
|
val viewState1 = userLiveLocationViewStateMapper.map(summary1)
|
||||||
|
val viewState2 = userLiveLocationViewStateMapper.map(summary2)
|
||||||
|
val viewState3 = userLiveLocationViewStateMapper.map(summaryWithoutLocation)
|
||||||
|
|
||||||
|
viewState1 shouldBeEqualTo null
|
||||||
|
viewState2 shouldBeEqualTo null
|
||||||
|
viewState3 shouldBeEqualTo null
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given a summary with valid data then result is correctly mapped`() = runTest {
|
||||||
|
val pinDrawable = mockk<Drawable>()
|
||||||
|
|
||||||
|
val locationDataContent = MessageBeaconLocationDataContent(
|
||||||
|
locationInfo = LocationInfo(geoUri = A_GEO_URI)
|
||||||
|
)
|
||||||
|
val summary = LiveLocationShareAggregatedSummary(
|
||||||
|
userId = A_USER_ID,
|
||||||
|
isActive = A_IS_ACTIVE,
|
||||||
|
endOfLiveTimestampMillis = A_END_OF_LIVE_TIMESTAMP,
|
||||||
|
lastLocationDataContent = locationDataContent,
|
||||||
|
)
|
||||||
|
locationPinProvider.givenCreateForUserId(A_USER_ID, pinDrawable)
|
||||||
|
|
||||||
|
val viewState = userLiveLocationViewStateMapper.map(summary)
|
||||||
|
|
||||||
|
val expectedViewState = UserLiveLocationViewState(
|
||||||
|
userId = A_USER_ID,
|
||||||
|
pinDrawable = pinDrawable,
|
||||||
|
locationData = LocationData(
|
||||||
|
latitude = A_LATITUDE,
|
||||||
|
longitude = A_LONGITUDE,
|
||||||
|
uncertainty = A_UNCERTAINTY
|
||||||
|
),
|
||||||
|
endOfLiveTimestampMillis = A_END_OF_LIVE_TIMESTAMP
|
||||||
|
)
|
||||||
|
viewState shouldBeEqualTo expectedViewState
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2022 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 im.vector.app.test.fakes
|
||||||
|
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import im.vector.app.features.home.room.detail.timeline.helper.LocationPinProvider
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.invoke
|
||||||
|
import io.mockk.mockk
|
||||||
|
|
||||||
|
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) }
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2021 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 im.vector.app.test.fakes
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import org.matrix.android.sdk.api.session.room.location.LocationSharingService
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationShareAggregatedSummary
|
||||||
|
|
||||||
|
class FakeLocationSharingService : LocationSharingService by mockk() {
|
||||||
|
|
||||||
|
fun givenRunningLiveLocationShareSummaries(summaries: List<LiveLocationShareAggregatedSummary>):
|
||||||
|
LiveData<List<LiveLocationShareAggregatedSummary>> {
|
||||||
|
return MutableLiveData(summaries).also {
|
||||||
|
every { getRunningLiveLocationShareSummaries() } returns it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
27
vector/src/test/java/im/vector/app/test/fakes/FakeRoom.kt
Normal file
27
vector/src/test/java/im/vector/app/test/fakes/FakeRoom.kt
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2021 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 im.vector.app.test.fakes
|
||||||
|
|
||||||
|
import io.mockk.mockk
|
||||||
|
import org.matrix.android.sdk.api.session.room.Room
|
||||||
|
|
||||||
|
class FakeRoom(
|
||||||
|
private val fakeLocationSharingService: FakeLocationSharingService = FakeLocationSharingService(),
|
||||||
|
) : Room by mockk() {
|
||||||
|
|
||||||
|
override fun locationSharingService() = fakeLocationSharingService
|
||||||
|
}
|
@ -0,0 +1,27 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2021 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 im.vector.app.test.fakes
|
||||||
|
|
||||||
|
import io.mockk.mockk
|
||||||
|
import org.matrix.android.sdk.api.session.room.RoomService
|
||||||
|
|
||||||
|
class FakeRoomService(
|
||||||
|
private val fakeRoom: FakeRoom = FakeRoom()
|
||||||
|
) : RoomService by mockk() {
|
||||||
|
|
||||||
|
override fun getRoom(roomId: String) = fakeRoom
|
||||||
|
}
|
@ -33,7 +33,8 @@ class FakeSession(
|
|||||||
val fakeCryptoService: FakeCryptoService = FakeCryptoService(),
|
val fakeCryptoService: FakeCryptoService = FakeCryptoService(),
|
||||||
val fakeProfileService: FakeProfileService = FakeProfileService(),
|
val fakeProfileService: FakeProfileService = FakeProfileService(),
|
||||||
val fakeHomeServerCapabilitiesService: FakeHomeServerCapabilitiesService = FakeHomeServerCapabilitiesService(),
|
val fakeHomeServerCapabilitiesService: FakeHomeServerCapabilitiesService = FakeHomeServerCapabilitiesService(),
|
||||||
val fakeSharedSecretStorageService: FakeSharedSecretStorageService = FakeSharedSecretStorageService()
|
val fakeSharedSecretStorageService: FakeSharedSecretStorageService = FakeSharedSecretStorageService(),
|
||||||
|
private val fakeRoomService: FakeRoomService = FakeRoomService(),
|
||||||
) : Session by mockk(relaxed = true) {
|
) : Session by mockk(relaxed = true) {
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@ -48,6 +49,7 @@ class FakeSession(
|
|||||||
override fun profileService(): ProfileService = fakeProfileService
|
override fun profileService(): ProfileService = fakeProfileService
|
||||||
override fun homeServerCapabilitiesService(): HomeServerCapabilitiesService = fakeHomeServerCapabilitiesService
|
override fun homeServerCapabilitiesService(): HomeServerCapabilitiesService = fakeHomeServerCapabilitiesService
|
||||||
override fun sharedSecretStorageService() = fakeSharedSecretStorageService
|
override fun sharedSecretStorageService() = fakeSharedSecretStorageService
|
||||||
|
override fun roomService() = fakeRoomService
|
||||||
|
|
||||||
fun givenVectorStore(vectorSessionStore: VectorSessionStore) {
|
fun givenVectorStore(vectorSessionStore: VectorSessionStore) {
|
||||||
coEvery {
|
coEvery {
|
||||||
|
Loading…
Reference in New Issue
Block a user