From b788a82d0d5731d5175e6b525343acc2ce0017e5 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Thu, 28 Apr 2022 12:10:39 +0200 Subject: [PATCH] Adding live summary entity into annotation entity --- .../LiveLocationAggregatedSummary.kt | 5 +- .../livelocation/LiveLocationBeaconContent.kt | 11 ---- .../model/EventAnnotationsSummaryEntity.kt | 4 +- .../database/model/SessionRealmModule.kt | 2 + ...iveLocationAggregatedSummaryEntityQuery.kt | 46 +++++++++++++ .../EventRelationsAggregationProcessor.kt | 56 ++++++++++------ ...DefaultLiveLocationAggregationProcessor.kt | 64 +++++-------------- .../LiveLocationAggregationProcessor.kt | 21 ++++-- .../factory/LiveLocationMessageItemFactory.kt | 3 +- 9 files changed, 122 insertions(+), 90 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/LiveLocationAggregatedSummaryEntityQuery.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/livelocation/LiveLocationAggregatedSummary.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/livelocation/LiveLocationAggregatedSummary.kt index a92ddba430..2f2708d7c0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/livelocation/LiveLocationAggregatedSummary.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/livelocation/LiveLocationAggregatedSummary.kt @@ -16,7 +16,7 @@ package org.matrix.android.sdk.api.session.room.model.livelocation -import org.matrix.android.sdk.api.session.room.model.message.LocationInfo +import org.matrix.android.sdk.api.session.room.model.message.MessageLocationContent /** * Aggregation info concerning a live location share. @@ -26,7 +26,8 @@ data class LiveLocationAggregatedSummary( * Event id of the event that started the live. */ val eventId: String, + val roomId: String, val isLive: Boolean, val endOfLiveTimestampAsMilliseconds: Long, - val lastLocation: LocationInfo? = null + val lastLocationContent: MessageLocationContent? = null, ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/livelocation/LiveLocationBeaconContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/livelocation/LiveLocationBeaconContent.kt index a7c78f6e80..54a781e2c8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/livelocation/LiveLocationBeaconContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/livelocation/LiveLocationBeaconContent.kt @@ -22,7 +22,6 @@ import org.matrix.android.sdk.api.session.events.model.Content import org.matrix.android.sdk.api.session.room.model.message.LocationAsset import org.matrix.android.sdk.api.session.room.model.message.LocationAssetType import org.matrix.android.sdk.api.session.room.model.message.MessageContent -import org.matrix.android.sdk.api.session.room.model.message.MessageLiveLocationContent import org.matrix.android.sdk.api.session.room.model.message.MessageType import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent @@ -61,16 +60,6 @@ data class LiveLocationBeaconContent( */ @Json(name = "org.matrix.msc3488.asset") val unstableLocationAsset: LocationAsset = LocationAsset(LocationAssetType.SELF), @Json(name = "m.asset") val locationAsset: LocationAsset? = null, - - /** - * Client side tracking of the last location - */ - var lastLocationContent: MessageLiveLocationContent? = null, - - /** - * Client side tracking of whether the beacon has timed out. - */ - var hasTimedOut: Boolean = false ) : MessageContent { fun getBestTimestampAsMilliseconds() = timestampAsMilliseconds ?: unstableTimestampAsMilliseconds diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventAnnotationsSummaryEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventAnnotationsSummaryEntity.kt index 3e88130420..537c39be32 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventAnnotationsSummaryEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventAnnotationsSummaryEntity.kt @@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.database.model import io.realm.RealmList import io.realm.RealmObject import io.realm.annotations.PrimaryKey +import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationAggregatedSummaryEntity import timber.log.Timber internal open class EventAnnotationsSummaryEntity( @@ -27,7 +28,8 @@ internal open class EventAnnotationsSummaryEntity( var reactionsSummary: RealmList = RealmList(), var editSummary: EditAggregatedSummaryEntity? = null, var referencesSummaryEntity: ReferencesAggregatedSummaryEntity? = null, - var pollResponseSummary: PollResponseAggregatedSummaryEntity? = null + var pollResponseSummary: PollResponseAggregatedSummaryEntity? = null, + var liveLocationAggregatedSummary: LiveLocationAggregatedSummaryEntity? = null, ) : RealmObject() { /** diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt index d0d23dd491..210d0989fe 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt @@ -17,6 +17,7 @@ package org.matrix.android.sdk.internal.database.model import io.realm.annotations.RealmModule +import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationAggregatedSummaryEntity import org.matrix.android.sdk.internal.database.model.presence.UserPresenceEntity import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntity @@ -47,6 +48,7 @@ import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntit EditAggregatedSummaryEntity::class, EditionOfEvent::class, PollResponseAggregatedSummaryEntity::class, + LiveLocationAggregatedSummaryEntity::class, ReferencesAggregatedSummaryEntity::class, PushRulesEntity::class, PushRuleEntity::class, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/LiveLocationAggregatedSummaryEntityQuery.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/LiveLocationAggregatedSummaryEntityQuery.kt new file mode 100644 index 0000000000..3be2ae9117 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/LiveLocationAggregatedSummaryEntityQuery.kt @@ -0,0 +1,46 @@ +/* + * 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 org.matrix.android.sdk.internal.database.query + +import io.realm.Realm +import io.realm.RealmQuery +import io.realm.kotlin.where +import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity +import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationAggregatedSummaryEntity +import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationAggregatedSummaryEntityFields + +internal fun LiveLocationAggregatedSummaryEntity.Companion.where(realm: Realm, roomId: String, eventId: String): RealmQuery { + return realm.where() + .equalTo(LiveLocationAggregatedSummaryEntityFields.ROOM_ID, roomId) + .equalTo(LiveLocationAggregatedSummaryEntityFields.EVENT_ID, eventId) +} + +internal fun LiveLocationAggregatedSummaryEntity.Companion.create(realm: Realm, roomId: String, eventId: String): LiveLocationAggregatedSummaryEntity { + val obj = realm.createObject(LiveLocationAggregatedSummaryEntity::class.java).apply { + this.eventId = eventId + this.roomId = roomId + } + val annotationSummary = EventAnnotationsSummaryEntity.getOrCreate(realm, roomId = roomId, eventId = eventId) + annotationSummary.liveLocationAggregatedSummary = obj + + return obj +} + +internal fun LiveLocationAggregatedSummaryEntity.Companion.getOrCreate(realm: Realm, roomId: String, eventId: String): LiveLocationAggregatedSummaryEntity { + return LiveLocationAggregatedSummaryEntity.where(realm, roomId, eventId).findFirst() + ?: LiveLocationAggregatedSummaryEntity.create(realm, roomId, eventId) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt index ac944ea8a7..a5f1bc5d43 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt @@ -33,6 +33,7 @@ import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent import org.matrix.android.sdk.api.session.room.model.ReferencesAggregatedContent import org.matrix.android.sdk.api.session.room.model.VoteInfo import org.matrix.android.sdk.api.session.room.model.VoteSummary +import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationBeaconContent import org.matrix.android.sdk.api.session.room.model.message.MessageContent import org.matrix.android.sdk.api.session.room.model.message.MessageEndPollContent import org.matrix.android.sdk.api.session.room.model.message.MessageLiveLocationContent @@ -91,7 +92,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor( // EventType.KEY_VERIFICATION_READY, EventType.KEY_VERIFICATION_KEY, EventType.ENCRYPTED - ) + EventType.POLL_START + EventType.POLL_RESPONSE + EventType.POLL_END + EventType.BEACON_LOCATION_DATA + ) + EventType.POLL_START + EventType.POLL_RESPONSE + EventType.POLL_END + EventType.STATE_ROOM_BEACON_INFO + EventType.BEACON_LOCATION_DATA override fun shouldProcess(eventId: String, eventType: String, insertType: EventInsertType): Boolean { return allowedTypes.contains(eventType) @@ -106,12 +107,12 @@ internal class EventRelationsAggregationProcessor @Inject constructor( } val isLocalEcho = LocalEcho.isLocalEchoId(event.eventId ?: "") when (event.type) { - EventType.REACTION -> { + EventType.REACTION -> { // we got a reaction!! Timber.v("###REACTION in room $roomId , reaction eventID ${event.eventId}") handleReaction(realm, event, roomId, isLocalEcho) } - EventType.MESSAGE -> { + EventType.MESSAGE -> { if (event.unsignedData?.relations?.annotations != null) { Timber.v("###REACTION Aggregation in room $roomId for event ${event.eventId}") handleInitialAggregatedRelations(realm, event, roomId, event.unsignedData.relations.annotations) @@ -137,7 +138,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor( EventType.KEY_VERIFICATION_START, EventType.KEY_VERIFICATION_MAC, EventType.KEY_VERIFICATION_READY, - EventType.KEY_VERIFICATION_KEY -> { + EventType.KEY_VERIFICATION_KEY -> { Timber.v("## SAS REF in room $roomId for event ${event.eventId}") event.content.toModel()?.relatesTo?.let { if (it.type == RelationType.REFERENCE && it.eventId != null) { @@ -146,7 +147,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor( } } - EventType.ENCRYPTED -> { + EventType.ENCRYPTED -> { // Relation type is in clear val encryptedEventContent = event.content.toModel() if (encryptedEventContent?.relatesTo?.type == RelationType.REPLACE || @@ -172,23 +173,28 @@ internal class EventRelationsAggregationProcessor @Inject constructor( EventType.KEY_VERIFICATION_START, EventType.KEY_VERIFICATION_MAC, EventType.KEY_VERIFICATION_READY, - EventType.KEY_VERIFICATION_KEY -> { + EventType.KEY_VERIFICATION_KEY -> { Timber.v("## SAS REF in room $roomId for event ${event.eventId}") encryptedEventContent.relatesTo.eventId?.let { handleVerification(realm, event, roomId, isLocalEcho, it) } } - in EventType.POLL_RESPONSE -> { + in EventType.POLL_RESPONSE -> { event.getClearContent().toModel(catchError = true)?.let { handleResponse(realm, event, it, roomId, isLocalEcho, event.getRelationContent()?.eventId) } } - in EventType.POLL_END -> { + in EventType.POLL_END -> { event.content.toModel(catchError = true)?.let { handleEndPoll(realm, event, it, roomId, isLocalEcho) } } - in EventType.BEACON_LOCATION_DATA -> { + in EventType.STATE_ROOM_BEACON_INFO -> { + event.content.toModel(catchError = true)?.let { + liveLocationAggregationProcessor.handleBeaconInfo(realm, event, it, roomId, isLocalEcho) + } + } + in EventType.BEACON_LOCATION_DATA -> { event.content.toModel(catchError = true)?.let { liveLocationAggregationProcessor.handleLiveLocation(realm, event, it, roomId, isLocalEcho) } @@ -213,7 +219,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor( // } // } } - EventType.REDACTION -> { + EventType.REDACTION -> { val eventToPrune = event.redacts?.let { EventEntity.where(realm, eventId = it).findFirst() } ?: return when (eventToPrune.type) { @@ -233,7 +239,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor( } } } - in EventType.POLL_START -> { + in EventType.POLL_START -> { val content: MessagePollContent? = event.content.toModel() if (content?.relatesTo?.type == RelationType.REPLACE) { Timber.v("###REPLACE in room $roomId for event ${event.eventId}") @@ -241,22 +247,27 @@ internal class EventRelationsAggregationProcessor @Inject constructor( handleReplace(realm, event, content, roomId, isLocalEcho) } } - in EventType.POLL_RESPONSE -> { + in EventType.POLL_RESPONSE -> { event.content.toModel(catchError = true)?.let { handleResponse(realm, event, it, roomId, isLocalEcho) } } - in EventType.POLL_END -> { + in EventType.POLL_END -> { event.content.toModel(catchError = true)?.let { handleEndPoll(realm, event, it, roomId, isLocalEcho) } } - in EventType.BEACON_LOCATION_DATA -> { + in EventType.STATE_ROOM_BEACON_INFO -> { + event.content.toModel(catchError = true)?.let { + liveLocationAggregationProcessor.handleBeaconInfo(realm, event, it, roomId, isLocalEcho) + } + } + in EventType.BEACON_LOCATION_DATA -> { event.content.toModel(catchError = true)?.let { liveLocationAggregationProcessor.handleLiveLocation(realm, event, it, roomId, isLocalEcho) } } - else -> Timber.v("UnHandled event ${event.eventId}") + else -> Timber.v("UnHandled event ${event.eventId}") } } catch (t: Throwable) { Timber.e(t, "## Should not happen ") @@ -325,7 +336,8 @@ internal class EventRelationsAggregationProcessor @Inject constructor( totalVotes = 0, winnerVoteCount = 0, ) - .toContent()) + .toContent() + ) } val txId = event.unsignedData?.transactionId @@ -729,11 +741,13 @@ internal class EventRelationsAggregationProcessor @Inject constructor( EventType.KEY_VERIFICATION_READY, EventType.KEY_VERIFICATION_KEY, EventType.KEY_VERIFICATION_MAC -> currentState.toState(VerificationState.WAITING) - EventType.KEY_VERIFICATION_CANCEL -> currentState.toState(if (event.senderId == userId) { - VerificationState.CANCELED_BY_ME - } else { - VerificationState.CANCELED_BY_OTHER - }) + EventType.KEY_VERIFICATION_CANCEL -> currentState.toState( + if (event.senderId == userId) { + VerificationState.CANCELED_BY_ME + } else { + VerificationState.CANCELED_BY_OTHER + } + ) EventType.KEY_VERIFICATION_DONE -> currentState.toState(VerificationState.DONE) else -> VerificationState.REQUEST } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/DefaultLiveLocationAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/DefaultLiveLocationAggregationProcessor.kt index 8de0965b40..f107fdb55c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/DefaultLiveLocationAggregationProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/DefaultLiveLocationAggregationProcessor.kt @@ -17,69 +17,35 @@ package org.matrix.android.sdk.internal.session.room.aggregation.livelocation import io.realm.Realm -import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.events.model.Event -import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.api.session.events.model.toContent -import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationBeaconContent import org.matrix.android.sdk.api.session.room.model.message.MessageLiveLocationContent -import org.matrix.android.sdk.internal.database.mapper.ContentMapper -import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity -import org.matrix.android.sdk.internal.database.query.getOrNull -import timber.log.Timber import javax.inject.Inject internal class DefaultLiveLocationAggregationProcessor @Inject constructor() : LiveLocationAggregationProcessor { - override fun handleLiveLocation(realm: Realm, event: Event, content: MessageLiveLocationContent, roomId: String, isLocalEcho: Boolean) { - val locationSenderId = event.senderId ?: return + override fun handleBeaconInfo(realm: Realm, event: Event, content: LiveLocationBeaconContent, roomId: String, isLocalEcho: Boolean) { + //val locationSenderId = event.senderId ?: return // We shouldn't process local echos if (isLocalEcho) { return } - // A beacon info state event has to be sent before sending location - // TODO handle missing check of m_relatesTo field - var beaconInfoEntity: CurrentStateEventEntity? = null - val eventTypesIterator = EventType.STATE_ROOM_BEACON_INFO.iterator() - while (beaconInfoEntity == null && eventTypesIterator.hasNext()) { - beaconInfoEntity = CurrentStateEventEntity.getOrNull(realm, roomId, locationSenderId, eventTypesIterator.next()) - } - - if (beaconInfoEntity == null) { - Timber.v("## LIVE LOCATION. There is not any beacon info which should be emitted before sending location updates") - return - } - val beaconInfoContent = ContentMapper.map(beaconInfoEntity.root?.content)?.toModel(catchError = true) - if (beaconInfoContent == null) { - Timber.v("## LIVE LOCATION. Beacon info content is invalid") - return - } - - // Check if live location is ended - if (!beaconInfoContent.isLive.orFalse()) { - Timber.v("## LIVE LOCATION. Beacon info is not live anymore") - return - } - - // Check if beacon info is outdated - if (isBeaconInfoOutdated(beaconInfoContent, content)) { - Timber.v("## LIVE LOCATION. Beacon info has timeout") - beaconInfoContent.hasTimedOut = true - } else { - beaconInfoContent.lastLocationContent = content - } - - beaconInfoEntity.root?.content = ContentMapper.map(beaconInfoContent.toContent()) + // TODO if live field is true, get eventId else get get replace eventId + // TODO getOrCreate existing aggregated summary + // TODO update the endOfLiveTimestamp and live fields } - private fun isBeaconInfoOutdated(beaconInfoContent: LiveLocationBeaconContent, - liveLocationContent: MessageLiveLocationContent): Boolean { - val beaconInfoStartTime = beaconInfoContent.getBestTimestampAsMilliseconds() ?: 0 - val liveLocationEventTime = liveLocationContent.getBestTimestampAsMilliseconds() ?: 0 - val timeout = beaconInfoContent.timeout ?: 0 - return liveLocationEventTime - beaconInfoStartTime > timeout + override fun handleLiveLocation(realm: Realm, event: Event, content: MessageLiveLocationContent, roomId: String, isLocalEcho: Boolean) { + //val locationSenderId = event.senderId ?: return + + // We shouldn't process local echos + if (isLocalEcho) { + return + } + + // TODO getOrCreate existing aggregated summary + // TODO add location content only if more recent than the current one if any } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessor.kt index 7b5f23e243..9b3025e2d9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessor.kt @@ -18,12 +18,23 @@ package org.matrix.android.sdk.internal.session.room.aggregation.livelocation import io.realm.Realm import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationBeaconContent import org.matrix.android.sdk.api.session.room.model.message.MessageLiveLocationContent internal interface LiveLocationAggregationProcessor { - fun handleLiveLocation(realm: Realm, - event: Event, - content: MessageLiveLocationContent, - roomId: String, - isLocalEcho: Boolean) + fun handleBeaconInfo( + realm: Realm, + event: Event, + content: LiveLocationBeaconContent, + roomId: String, + isLocalEcho: Boolean, + ) + + fun handleLiveLocation( + realm: Realm, + event: Event, + content: MessageLiveLocationContent, + roomId: String, + isLocalEcho: Boolean, + ) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/LiveLocationMessageItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/LiveLocationMessageItemFactory.kt index c417038935..39cba3b8dc 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/LiveLocationMessageItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/LiveLocationMessageItemFactory.kt @@ -46,7 +46,8 @@ class LiveLocationMessageItemFactory @Inject constructor( } private fun isLiveRunning(liveLocationContent: LiveLocationBeaconContent): Boolean { - return liveLocationContent.isLive.orFalse() && liveLocationContent.hasTimedOut.not() + // TODO when we will use aggregatedSummary, check if the live has timed out as well + return liveLocationContent.isLive.orFalse() } private fun buildStartLiveItem(