mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2024-11-25 02:45:53 +03:00
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
34 changed files with 1071 additions and 27 deletions
|
@ -1 +1,2 @@
|
|||
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.call.RoomCallService
|
||||
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.model.RoomSummary
|
||||
import org.matrix.android.sdk.api.session.room.model.relation.RelationService
|
||||
|
@ -163,4 +164,9 @@ interface Room {
|
|||
* Get the RoomVersionService associated to this Room.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
data class LiveLocationShareAggregatedSummary(
|
||||
val userId: String?,
|
||||
/**
|
||||
* 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.MigrateSessionTo027
|
||||
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 timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
@ -60,7 +61,7 @@ internal class RealmSessionStoreMigration @Inject constructor(
|
|||
override fun equals(other: Any?) = other is RealmSessionStoreMigration
|
||||
override fun hashCode() = 1000
|
||||
|
||||
val schemaVersion = 28L
|
||||
val schemaVersion = 29L
|
||||
|
||||
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
|
||||
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 < 27) MigrateSessionTo027(realm).perform()
|
||||
if (oldVersion < 28) MigrateSessionTo028(realm).perform()
|
||||
if (oldVersion < 29) MigrateSessionTo029(realm).perform()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -58,7 +58,7 @@ internal object EventAnnotationsSummaryMapper {
|
|||
PollResponseAggregatedSummaryEntityMapper.map(it)
|
||||
},
|
||||
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.message.MessageBeaconLocationDataContent
|
||||
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 {
|
||||
return LiveLocationShareAggregatedSummary(
|
||||
userId = entity.userId,
|
||||
isActive = entity.isActive,
|
||||
endOfLiveTimestampMillis = entity.endOfLiveTimestampMillis,
|
||||
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 userId: String = "",
|
||||
|
||||
/**
|
||||
* Indicate whether the live is currently running.
|
||||
*/
|
||||
|
|
|
@ -28,9 +28,15 @@ internal fun LiveLocationShareAggregatedSummaryEntity.Companion.where(
|
|||
roomId: String,
|
||||
eventId: String,
|
||||
): 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>()
|
||||
.equalTo(LiveLocationShareAggregatedSummaryEntityFields.ROOM_ID, roomId)
|
||||
.equalTo(LiveLocationShareAggregatedSummaryEntityFields.EVENT_ID, eventId)
|
||||
}
|
||||
|
||||
internal fun LiveLocationShareAggregatedSummaryEntity.Companion.create(
|
||||
|
@ -63,3 +69,31 @@ internal fun LiveLocationShareAggregatedSummaryEntity.Companion.get(
|
|||
): LiveLocationShareAggregatedSummaryEntity? {
|
||||
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.call.RoomCallService
|
||||
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.model.RoomSummary
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomType
|
||||
|
@ -69,6 +70,7 @@ internal class DefaultRoom(
|
|||
private val roomAccountDataService: RoomAccountDataService,
|
||||
private val roomVersionService: RoomVersionService,
|
||||
private val viaParameterFinder: ViaParameterFinder,
|
||||
private val locationSharingService: LocationSharingService,
|
||||
override val coroutineDispatchers: MatrixCoroutineDispatchers
|
||||
) : Room {
|
||||
|
||||
|
@ -104,4 +106,5 @@ internal class DefaultRoom(
|
|||
override fun roomPushRuleService() = roomPushRuleService
|
||||
override fun roomAccountDataService() = roomAccountDataService
|
||||
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.crypto.DefaultRoomCryptoService
|
||||
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.notification.DefaultRoomPushRuleService
|
||||
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 roomAccountDataServiceFactory: DefaultRoomAccountDataService.Factory,
|
||||
private val viaParameterFinder: ViaParameterFinder,
|
||||
private val locationSharingServiceFactory: DefaultLocationSharingService.Factory,
|
||||
private val coroutineDispatchers: MatrixCoroutineDispatchers
|
||||
) : RoomFactory {
|
||||
|
||||
|
@ -96,6 +98,7 @@ internal class DefaultRoomFactory @Inject constructor(
|
|||
roomAccountDataService = roomAccountDataServiceFactory.create(roomId),
|
||||
roomVersionService = roomVersionServiceFactory.create(roomId),
|
||||
viaParameterFinder = viaParameterFinder,
|
||||
locationSharingService = locationSharingServiceFactory.create(roomId),
|
||||
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.internal.database.mapper.ContentMapper
|
||||
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.di.SessionId
|
||||
import org.matrix.android.sdk.internal.di.WorkManagerProvider
|
||||
|
@ -35,6 +36,7 @@ import timber.log.Timber
|
|||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
|
||||
// TODO add unit tests
|
||||
internal class LiveLocationAggregationProcessor @Inject constructor(
|
||||
@SessionId private val sessionId: String,
|
||||
private val workManagerProvider: WorkManagerProvider,
|
||||
|
@ -70,6 +72,9 @@ internal class LiveLocationAggregationProcessor @Inject constructor(
|
|||
val endOfLiveTimestampMillis = content.getBestTimestampMillis()?.let { it + (content.timeout ?: 0) }
|
||||
aggregatedSummary.endOfLiveTimestampMillis = endOfLiveTimestampMillis
|
||||
aggregatedSummary.isActive = isLive
|
||||
aggregatedSummary.userId = event.senderId
|
||||
|
||||
deactivateAllPreviousBeacons(realm, roomId, event.senderId, targetEventId)
|
||||
|
||||
if (isLive) {
|
||||
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
|
||||
}
|
||||
|
|
|
@ -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.invite.InviteUsersToRoomViewModel
|
||||
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.login2.LoginViewModel2
|
||||
import im.vector.app.features.login2.created.AccountCreatedViewModel
|
||||
|
@ -600,4 +601,9 @@ interface MavericksViewModelModule {
|
|||
@IntoMap
|
||||
@MavericksViewModelKey(VectorAttachmentViewerViewModel::class)
|
||||
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) {
|
||||
views.mapView.zoomToLocation(event.userLocation.latitude, event.userLocation.longitude)
|
||||
views.mapView.zoomToLocation(event.userLocation)
|
||||
}
|
||||
|
||||
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.marginTop
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import com.mapbox.mapboxsdk.camera.CameraPosition
|
||||
import com.mapbox.mapboxsdk.geometry.LatLng
|
||||
import com.mapbox.mapboxsdk.maps.MapView
|
||||
import com.mapbox.mapboxsdk.maps.MapboxMap
|
||||
|
@ -164,7 +163,7 @@ class MapTilerMapView @JvmOverloads constructor(
|
|||
|
||||
state.userLocationData?.let { locationData ->
|
||||
if (!initZoomDone || !state.zoomOnlyOnce) {
|
||||
zoomToLocation(locationData.latitude, locationData.longitude)
|
||||
zoomToLocation(locationData)
|
||||
initZoomDone = true
|
||||
}
|
||||
|
||||
|
@ -180,12 +179,9 @@ class MapTilerMapView @JvmOverloads constructor(
|
|||
}
|
||||
}
|
||||
|
||||
fun zoomToLocation(latitude: Double, longitude: Double) {
|
||||
fun zoomToLocation(locationData: LocationData) {
|
||||
Timber.d("## Location: zoomToLocation")
|
||||
mapRefs?.map?.cameraPosition = CameraPosition.Builder()
|
||||
.target(LatLng(latitude, longitude))
|
||||
.zoom(INITIAL_MAP_ZOOM_IN_PREVIEW)
|
||||
.build()
|
||||
mapRefs?.map?.zoomToLocation(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
|
||||
|
||||
import android.os.Bundle
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
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.Style
|
||||
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 im.vector.app.R
|
||||
import im.vector.app.core.extensions.addChildFragment
|
||||
import im.vector.app.core.platform.VectorBaseFragment
|
||||
import im.vector.app.databinding.FragmentSimpleContainerBinding
|
||||
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
|
||||
|
||||
/**
|
||||
* 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
|
||||
class LocationLiveMapViewFragment : VectorBaseFragment<FragmentSimpleContainerBinding>() {
|
||||
class LocationLiveMapViewFragment @Inject constructor() : VectorBaseFragment<FragmentSimpleContainerBinding>() {
|
||||
|
||||
@Inject
|
||||
lateinit var urlMapProvider: UrlMapProvider
|
||||
@Inject 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 {
|
||||
return FragmentSimpleContainerBinding.inflate(layoutInflater, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
setupMap()
|
||||
}
|
||||
|
||||
private fun setupMap() {
|
||||
val mapFragment = getOrCreateSupportMapFragment()
|
||||
|
||||
mapFragment.getMapAsync { mapBoxMap ->
|
||||
lifecycleScope.launchWhenCreated {
|
||||
mapBoxMap.setStyle(urlMapProvider.getMapUrl())
|
||||
mapFragment.getMapAsync { mapboxMap ->
|
||||
lifecycleScope.launch {
|
||||
mapboxMap.setStyle(urlMapProvider.getMapUrl()) { style ->
|
||||
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) }
|
||||
}
|
||||
|
||||
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 {
|
||||
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 fakeProfileService: FakeProfileService = FakeProfileService(),
|
||||
val fakeHomeServerCapabilitiesService: FakeHomeServerCapabilitiesService = FakeHomeServerCapabilitiesService(),
|
||||
val fakeSharedSecretStorageService: FakeSharedSecretStorageService = FakeSharedSecretStorageService()
|
||||
val fakeSharedSecretStorageService: FakeSharedSecretStorageService = FakeSharedSecretStorageService(),
|
||||
private val fakeRoomService: FakeRoomService = FakeRoomService(),
|
||||
) : Session by mockk(relaxed = true) {
|
||||
|
||||
init {
|
||||
|
@ -48,6 +49,7 @@ class FakeSession(
|
|||
override fun profileService(): ProfileService = fakeProfileService
|
||||
override fun homeServerCapabilitiesService(): HomeServerCapabilitiesService = fakeHomeServerCapabilitiesService
|
||||
override fun sharedSecretStorageService() = fakeSharedSecretStorageService
|
||||
override fun roomService() = fakeRoomService
|
||||
|
||||
fun givenVectorStore(vectorSessionStore: VectorSessionStore) {
|
||||
coEvery {
|
||||
|
|
Loading…
Reference in a new issue