| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919 |
- package ch.threema.data.models
- import ch.threema.app.ThreemaApplication
- import ch.threema.app.managers.CoreServiceManager
- import ch.threema.app.managers.ListenerManager
- import ch.threema.app.managers.ServiceManager
- import ch.threema.app.services.ContactService
- import ch.threema.app.services.DeadlineListService.DEADLINE_INDEFINITE_EXCEPT_MENTIONS
- import ch.threema.app.tasks.ReflectContactSyncUpdateImmediateTask
- import ch.threema.app.tasks.ReflectContactSyncUpdateTask
- import ch.threema.app.utils.ContactUtil
- import ch.threema.base.utils.getThreemaLogger
- import ch.threema.common.toDate
- import ch.threema.data.datatypes.AndroidContactLookupInfo
- import ch.threema.data.datatypes.AvailabilityStatus
- import ch.threema.data.datatypes.IdColor
- import ch.threema.data.datatypes.NotificationTriggerPolicyOverride
- import ch.threema.data.repositories.RepositoryToken
- import ch.threema.data.storage.DatabaseBackend
- import ch.threema.data.storage.DbContact
- import ch.threema.domain.models.ContactSyncState
- import ch.threema.domain.models.IdentityState
- import ch.threema.domain.models.IdentityType
- import ch.threema.domain.models.ReadReceiptPolicy
- import ch.threema.domain.models.TypingIndicatorPolicy
- import ch.threema.domain.models.VerificationLevel
- import ch.threema.domain.models.WorkVerificationLevel
- import ch.threema.domain.protocol.ThreemaFeature
- import ch.threema.domain.taskmanager.ActiveTaskCodec
- import ch.threema.domain.types.IdentityString
- import ch.threema.storage.models.ContactModel.AcquaintanceLevel
- import java.time.Instant
- import java.util.Date
- import kotlinx.coroutines.flow.MutableStateFlow
- private val logger = getThreemaLogger("data.ContactModel")
- /**
- * A contact.
- */
- class ContactModel(
- val identity: IdentityString,
- data: ContactModelData,
- private val databaseBackend: DatabaseBackend,
- coreServiceManager: CoreServiceManager,
- ) : BaseModel<ContactModelData, ReflectContactSyncUpdateTask>(
- MutableStateFlow(data),
- "ContactModel",
- coreServiceManager.multiDeviceManager,
- coreServiceManager.taskManager,
- ) {
- init {
- require(identity == data.identity) {
- "Contact model identity mismatch"
- }
- require(identity != coreServiceManager.identityStore.getIdentityString()) {
- "Cannot create contact model with the identity of the user"
- }
- }
- /**
- * We have to make the bridge over to the old ContactService in order
- * to keep the new and old caches both correct.
- *
- * TODO(ANDR-4361): Remove this
- */
- private val deprecatedContactService: ContactService? by lazy {
- val serviceManager: ServiceManager? = ThreemaApplication.getServiceManager()
- if (serviceManager == null) {
- logger.warn("Tried to get the contactService before the service-manager was created.")
- }
- serviceManager?.contactService
- }
- /**
- * Update the contact's first and last name.
- *
- * @throws [ModelDeletedException] if model is deleted.
- */
- fun setNameFromLocal(firstName: String, lastName: String) {
- this.updateFields(
- methodName = "setNameFromLocal",
- detectChanges = { originalData -> originalData.firstName != firstName || originalData.lastName != lastName },
- updateData = { originalData ->
- originalData.copy(
- firstName = firstName,
- lastName = lastName,
- )
- },
- updateDatabase = ::updateDatabase,
- onUpdated = ::defaultOnUpdated,
- reflectUpdateTask = ReflectContactSyncUpdateTask.ReflectNameUpdate(
- newFirstName = firstName,
- newLastName = lastName,
- contactIdentity = identity,
- ),
- )
- }
- /**
- * Update the contact's jobTitle
- *
- * @throws ModelDeletedException if model is deleted.
- *
- * TODO(ANDR-3611): Reflect change to device group
- */
- fun setJobTitleFromLocal(jobTitle: String?) {
- this.updateFields(
- methodName = "setJobTitleFromLocal",
- detectChanges = { originalData -> originalData.jobTitle != jobTitle },
- updateData = { originalData -> originalData.copy(jobTitle = jobTitle) },
- updateDatabase = ::updateDatabase,
- onUpdated = ::defaultOnUpdated,
- reflectUpdateTask = null,
- )
- }
- /**
- * Update the contact's department
- *
- * @throws ModelDeletedException if model is deleted.
- *
- * TODO(ANDR-3611): Reflect change to device group
- */
- fun setDepartmentFromLocal(department: String?) {
- this.updateFields(
- methodName = "setDepartmentFromLocal",
- detectChanges = { originalData -> originalData.department != department },
- updateData = { originalData -> originalData.copy(department = department) },
- updateDatabase = ::updateDatabase,
- onUpdated = ::defaultOnUpdated,
- reflectUpdateTask = null,
- )
- }
- /**
- * Update the contact's acquaintance level.
- *
- * @throws [ModelDeletedException] if model is deleted.
- */
- fun setAcquaintanceLevelFromLocal(acquaintanceLevel: AcquaintanceLevel) {
- this.updateFields(
- methodName = "setAcquaintanceLevelFromLocal",
- detectChanges = { originalData -> originalData.acquaintanceLevel != acquaintanceLevel },
- updateData = { originalData -> originalData.copy(acquaintanceLevel = acquaintanceLevel) },
- updateDatabase = ::updateDatabase,
- onUpdated = { contactModelData ->
- deprecatedContactService?.invalidateCache(contactModelData.identity)
- when (acquaintanceLevel) {
- AcquaintanceLevel.DIRECT -> notifyDeprecatedOnModifiedListeners(contactModelData)
- AcquaintanceLevel.GROUP -> notifyDeprecatedOnRemovedListeners(contactModelData.identity)
- }
- },
- reflectUpdateTask = ReflectContactSyncUpdateTask.ReflectAcquaintanceLevelUpdate(
- acquaintanceLevel,
- identity,
- ),
- )
- }
- /**
- * Update the contact's verification level.
- *
- * @throws [ModelDeletedException] if model is deleted.
- */
- fun setVerificationLevelFromLocal(verificationLevel: VerificationLevel) {
- this.updateFields(
- methodName = "setVerificationLevelFromLocal",
- detectChanges = { originalData -> originalData.verificationLevel != verificationLevel },
- updateData = { originalData -> originalData.copy(verificationLevel = verificationLevel) },
- updateDatabase = ::updateDatabase,
- onUpdated = ::defaultOnUpdated,
- reflectUpdateTask = ReflectContactSyncUpdateTask.ReflectVerificationLevelUpdate(
- verificationLevel,
- identity,
- ),
- )
- }
- /**
- * Update the contact's work verification level.
- *
- * @throws [ModelDeletedException] if model is deleted.
- */
- fun setWorkVerificationLevelFromLocal(workVerificationLevel: WorkVerificationLevel) {
- this.updateFields(
- methodName = "setWorkVerificationLevelFromLocal",
- detectChanges = { originalData -> originalData.workVerificationLevel != workVerificationLevel },
- updateData = { originalData -> originalData.copy(workVerificationLevel = workVerificationLevel) },
- updateDatabase = ::updateDatabase,
- onUpdated = ::defaultOnUpdated,
- reflectUpdateTask = ReflectContactSyncUpdateTask.ReflectWorkVerificationLevelUpdate(
- workVerificationLevel,
- identity,
- ),
- )
- }
- /**
- * Update the contact's identity type.
- *
- * @throws [ModelDeletedException] if model is deleted.
- */
- fun setIdentityTypeFromLocal(identityType: IdentityType) {
- this.updateFields(
- methodName = "setIdentityTypeFromLocal",
- detectChanges = { originalData -> originalData.identityType != identityType },
- updateData = { originalData -> originalData.copy(identityType = identityType) },
- updateDatabase = ::updateDatabase,
- onUpdated = ::defaultOnUpdated,
- reflectUpdateTask = ReflectContactSyncUpdateTask.ReflectIdentityTypeUpdate(
- identityType,
- identity,
- ),
- )
- }
- fun setFeatureMaskFromLocal(
- featureMask: Long,
- ) {
- // Warn the user in case there is no forward security support anymore (indicated by a
- // feature mask change).
- data?.let {
- val previousFSSupport = ThreemaFeature.canForwardSecurity(it.featureMaskLong())
- val newFSSupport = ThreemaFeature.canForwardSecurity(featureMask)
- if (previousFSSupport && !newFSSupport) {
- ContactUtil.onForwardSecurityNotSupportedAnymore(this)
- }
- }
- this.updateFields(
- methodName = "setFeatureMaskFromLocal",
- detectChanges = { originalData -> originalData.featureMask != featureMask.toULong() },
- updateData = { originalData -> originalData.copy(featureMask = featureMask.toULong()) },
- updateDatabase = ::updateDatabase,
- onUpdated = ::defaultOnUpdated,
- reflectUpdateTask = ReflectContactSyncUpdateTask.ReflectFeatureMaskUpdate(
- featureMask,
- identity,
- ),
- )
- }
- /**
- * Update the contact's `availabilityStatus` value **without** reflecting the change
- */
- fun setAvailabilityStatusFromSync(availabilityStatus: AvailabilityStatus) {
- this.updateFields(
- methodName = "setAvailabilityStatusFromSync",
- detectChanges = { originalData -> originalData.availabilityStatus != availabilityStatus },
- updateData = { originalData -> originalData.copy(availabilityStatus = availabilityStatus) },
- updateDatabase = ::updateDatabase,
- onUpdated = ::defaultOnUpdated,
- )
- }
- /**
- * Persist the contact's `availabilityStatus` locally
- */
- fun persistAvailabilityStatus(availabilityStatus: AvailabilityStatus) {
- this.updateFields(
- methodName = "persistAvailabilityStatus",
- detectChanges = { originalData -> originalData.availabilityStatus != availabilityStatus },
- updateData = { originalData -> originalData.copy(availabilityStatus = availabilityStatus) },
- updateDatabase = ::updateDatabase,
- onUpdated = ::defaultOnUpdated,
- )
- }
- /**
- * Update the contact's first name.
- *
- * @throws [ModelDeletedException] if model is deleted.
- */
- fun setFirstNameFromSync(firstName: String) {
- this.updateFields(
- methodName = "setFirstNameFromSync",
- detectChanges = { originalData -> originalData.firstName != firstName },
- updateData = { originalData -> originalData.copy(firstName = firstName) },
- updateDatabase = ::updateDatabase,
- onUpdated = ::defaultOnUpdated,
- )
- }
- /**
- * Update the contact's last name.
- *
- * @throws [ModelDeletedException] if model is deleted.
- */
- fun setLastNameFromSync(lastName: String) {
- this.updateFields(
- methodName = "setLastNameFromSync",
- detectChanges = { originalData -> originalData.lastName != lastName },
- updateData = { originalData -> originalData.copy(lastName = lastName) },
- updateDatabase = ::updateDatabase,
- onUpdated = ::defaultOnUpdated,
- )
- }
- /**
- * Update the contact's public nickname.
- *
- * @throws [ModelDeletedException] if model is deleted.
- */
- fun setNicknameFromSync(nickname: String?) {
- this.updateFields(
- methodName = "setNicknameFromSync",
- detectChanges = { originalData -> originalData.nickname != nickname },
- updateData = { originalData -> originalData.copy(nickname = nickname) },
- updateDatabase = ::updateDatabase,
- onUpdated = ::defaultOnUpdated,
- )
- }
- suspend fun setNicknameFromRemote(nickname: String, handle: ActiveTaskCodec) {
- logger.debug("Updating nickname of {} to {}", identity, nickname)
- // We check whether the nickname is different before trying to reflect it.
- val data = ensureNotDeleted("setNicknameFromRemote")
- if (data.nickname == nickname) {
- return
- }
- if (multiDeviceManager.isMultiDeviceActive) {
- ReflectContactSyncUpdateImmediateTask.ReflectContactNickname(
- contactIdentity = identity,
- newNickname = nickname,
- ).reflect(handle)
- }
- this.updateFields(
- methodName = "setNicknameFromRemote",
- detectChanges = { originalData -> originalData.nickname != nickname },
- updateData = { originalData -> originalData.copy(nickname = nickname) },
- updateDatabase = ::updateDatabase,
- onUpdated = ::defaultOnUpdated,
- )
- }
- /**
- * Update the contact's verification level.
- *
- * @throws [ModelDeletedException] if model is deleted.
- */
- fun setVerificationLevelFromSync(verificationLevel: VerificationLevel) {
- this.updateFields(
- methodName = "setVerificationLevelFromSync",
- detectChanges = { it.verificationLevel != verificationLevel },
- updateData = { it.copy(verificationLevel = verificationLevel) },
- updateDatabase = ::updateDatabase,
- onUpdated = ::defaultOnUpdated,
- )
- }
- /**
- * Update the contact's work verification level.
- *
- * @throws [ModelDeletedException] if model is deleted.
- */
- fun setWorkVerificationLevelFromSync(workVerificationLevel: WorkVerificationLevel) {
- this.updateFields(
- methodName = "setWorkVerificationLevelFromSync",
- detectChanges = { it.workVerificationLevel != workVerificationLevel },
- updateData = { it.copy(workVerificationLevel = workVerificationLevel) },
- updateDatabase = ::updateDatabase,
- onUpdated = ::defaultOnUpdated,
- )
- }
- /**
- * Update the contact's identity type.
- *
- * @throws [ModelDeletedException] if model is deleted.
- */
- fun setIdentityTypeFromSync(identityType: IdentityType) {
- this.updateFields(
- methodName = "setIdentityTypeFromSync",
- detectChanges = { it.identityType != identityType },
- updateData = { it.copy(identityType = identityType) },
- updateDatabase = ::updateDatabase,
- onUpdated = ::defaultOnUpdated,
- )
- }
- /**
- * Update the contact's acquaintance level.
- *
- * @throws [ModelDeletedException] if model is deleted.
- */
- fun setAcquaintanceLevelFromSync(acquaintanceLevel: AcquaintanceLevel) {
- this.updateFields(
- methodName = "setAcquaintanceLevelFromSync",
- detectChanges = { it.acquaintanceLevel != acquaintanceLevel },
- updateData = { it.copy(acquaintanceLevel = acquaintanceLevel) },
- updateDatabase = ::updateDatabase,
- onUpdated = ::defaultOnUpdated,
- )
- }
- /**
- * Update the contact's activity state.
- *
- * @throws [ModelDeletedException] if model is deleted.
- */
- fun setActivityStateFromSync(activityState: IdentityState) {
- this.updateFields(
- methodName = "setActivityStateFromSync",
- detectChanges = { it.activityState != activityState },
- updateData = { it.copy(activityState = activityState) },
- updateDatabase = ::updateDatabase,
- onUpdated = ::defaultOnUpdated,
- )
- }
- /**
- * Update the contact's activity state.
- *
- * @throws [ModelDeletedException] if model is deleted.
- */
- fun setActivityStateFromLocal(activityState: IdentityState) {
- this.updateFields(
- methodName = "setActivityStateFromLocal",
- detectChanges = { it.activityState != activityState },
- updateData = { it.copy(activityState = activityState) },
- updateDatabase = ::updateDatabase,
- onUpdated = ::defaultOnUpdated,
- reflectUpdateTask = ReflectContactSyncUpdateTask.ReflectActivityStateUpdate(
- activityState,
- identity,
- ),
- )
- }
- /**
- * Update the contact's feature mask.
- *
- * @throws [ModelDeletedException] if model is deleted.
- */
- fun setFeatureMaskFromSync(featureMask: ULong) {
- this.updateFields(
- methodName = "setFeatureMaskFromSync",
- detectChanges = { it.featureMask != featureMask },
- updateData = { it.copy(featureMask = featureMask) },
- updateDatabase = ::updateDatabase,
- onUpdated = ::defaultOnUpdated,
- )
- }
- /**
- * Update the contact's sync state.
- *
- * @throws [ModelDeletedException] if model is deleted.
- */
- fun setSyncStateFromSync(syncState: ContactSyncState) {
- this.updateFields(
- methodName = "setSyncStateFromSync",
- detectChanges = { it.syncState != syncState },
- updateData = { it.copy(syncState = syncState) },
- updateDatabase = ::updateDatabase,
- onUpdated = { updatedData ->
- // No need to notify listeners, this isn't something that will result in a UI change.
- // But keep old-service cache correct:
- deprecatedContactService?.invalidateCache(updatedData.identity)
- },
- )
- }
- /**
- * Update the contact's read receipt policy.
- *
- * @throws [ModelDeletedException] if model is deleted.
- */
- fun setReadReceiptPolicyFromSync(readReceiptPolicy: ReadReceiptPolicy) {
- this.updateFields(
- methodName = "setReadReceiptPolicyFromSync",
- detectChanges = { it.readReceiptPolicy != readReceiptPolicy },
- updateData = { it.copy(readReceiptPolicy = readReceiptPolicy) },
- updateDatabase = ::updateDatabase,
- onUpdated = ::defaultOnUpdated,
- )
- }
- /**
- * Update the contact's read receipt policy.
- *
- * @throws ModelDeletedException if model is deleted
- */
- fun setReadReceiptPolicyFromLocal(readReceiptPolicy: ReadReceiptPolicy) {
- this.updateFields(
- methodName = "setReadReceiptPolicyFromLocal",
- detectChanges = { it.readReceiptPolicy != readReceiptPolicy },
- updateData = { it.copy(readReceiptPolicy = readReceiptPolicy) },
- updateDatabase = ::updateDatabase,
- onUpdated = ::defaultOnUpdated,
- reflectUpdateTask = ReflectContactSyncUpdateTask.ReflectReadReceiptPolicyUpdate(
- readReceiptPolicy,
- identity,
- ),
- )
- }
- /**
- * Update the contact's typing indicator policy.
- *
- * @throws [ModelDeletedException] if model is deleted.
- */
- fun setTypingIndicatorPolicyFromSync(typingIndicatorPolicy: TypingIndicatorPolicy) {
- this.updateFields(
- methodName = "setTypingIndicatorPolicyFromSync",
- detectChanges = { it.typingIndicatorPolicy != typingIndicatorPolicy },
- updateData = { it.copy(typingIndicatorPolicy = typingIndicatorPolicy) },
- updateDatabase = ::updateDatabase,
- onUpdated = ::defaultOnUpdated,
- )
- }
- /**
- * Update the contact's typing indicator policy.
- *
- * @throws [ModelDeletedException] if model is deleted.
- */
- fun setTypingIndicatorPolicyFromLocal(typingIndicatorPolicy: TypingIndicatorPolicy) {
- this.updateFields(
- methodName = "setTypingIndicatorPolicyFromLocal",
- detectChanges = { it.typingIndicatorPolicy != typingIndicatorPolicy },
- updateData = { it.copy(typingIndicatorPolicy = typingIndicatorPolicy) },
- updateDatabase = ::updateDatabase,
- onUpdated = ::defaultOnUpdated,
- reflectUpdateTask = ReflectContactSyncUpdateTask.ReflectTypingIndicatorPolicyUpdate(
- typingIndicatorPolicy,
- identity,
- ),
- )
- }
- /**
- * Update or remove the contact's Android contact lookup key.
- *
- * @throws [ModelDeletedException] if model is deleted.
- */
- fun setAndroidContactLookupKey(lookupKey: AndroidContactLookupInfo) {
- this.updateFields(
- methodName = "setAndroidLookupKey",
- detectChanges = { originalData -> originalData.androidContactLookupInfo != lookupKey },
- updateData = { originalData -> originalData.copy(androidContactLookupInfo = lookupKey) },
- updateDatabase = ::updateDatabase,
- onUpdated = ::defaultOnUpdated,
- )
- }
- /**
- * Unlink the contact from the android contact. This sets the android lookup key to null and
- * downgrades the verification level if it is [VerificationLevel.SERVER_VERIFIED]. Note that
- * the verification level change is reflected if MD is active.
- *
- * @throws [ModelDeletedException] if model is deleted.
- */
- fun removeAndroidContactLink() {
- // Remove the android lookup info
- this.updateFields(
- methodName = "unlinkAndroidContact",
- detectChanges = { originalData -> originalData.androidContactLookupInfo != null },
- updateData = { originalData -> originalData.copy(androidContactLookupInfo = null) },
- updateDatabase = ::updateDatabase,
- onUpdated = ::defaultOnUpdated,
- )
- // Change verification level if it is server verified. Note that we do not use
- // setVerificationLevelFromLocal as this must only be done when the verification level is
- // server verified.
- this.updateFields(
- methodName = "unlinkAndroidContact",
- detectChanges = { originalData -> originalData.verificationLevel == VerificationLevel.SERVER_VERIFIED },
- updateData = { originalData -> originalData.copy(verificationLevel = VerificationLevel.UNVERIFIED) },
- updateDatabase = ::updateDatabase,
- onUpdated = ::defaultOnUpdated,
- reflectUpdateTask = ReflectContactSyncUpdateTask.ReflectVerificationLevelUpdate(
- VerificationLevel.UNVERIFIED,
- identity,
- ),
- )
- }
- /**
- * Set the id color to the given value. Note that normally it isn't necessary to set this manually.
- */
- fun setIdColor(idColor: IdColor) {
- this.updateFields(
- methodName = "setIdColor",
- detectChanges = { originalData -> originalData.idColor != idColor },
- updateData = { originalData -> originalData.copy(idColor = idColor) },
- updateDatabase = ::updateDatabase,
- onUpdated = null,
- )
- }
- /**
- * Update or remove the contact's local avatar expiration date.
- *
- * @throws [ModelDeletedException] if model is deleted.
- */
- fun setLocalAvatarExpires(expiresAt: Instant?) {
- setLocalAvatarExpires(expiresAt?.toDate())
- }
- private fun setLocalAvatarExpires(expiresAt: Date?) {
- this.updateFields(
- methodName = "setLocalAvatarExpires",
- detectChanges = { originalData -> originalData.localAvatarExpires != expiresAt },
- updateData = { originalData -> originalData.copy(localAvatarExpires = expiresAt) },
- updateDatabase = ::updateDatabase,
- onUpdated = { updatedData ->
- // No need to notify listeners, this isn't something that will result in a UI change.
- // But keep old-service cache correct:
- deprecatedContactService?.invalidateCache(updatedData.identity)
- },
- )
- }
- /**
- * Clear the "isRestored" flag on the contact.
- *
- * This should be called once the post-restore sync steps (e.g. profile picture request)
- * have been completed.
- *
- * @throws [ModelDeletedException] if model is deleted.
- */
- fun clearIsRestored() {
- this.updateFields(
- methodName = "clearIsRestored",
- detectChanges = { originalData -> originalData.isRestored },
- updateData = { originalData -> originalData.copy(isRestored = false) },
- updateDatabase = ::updateDatabase,
- onUpdated = { updatedData ->
- // No need to notify listeners, this isn't something that will result in a UI change.
- // But keep old-service cache correct:
- deprecatedContactService?.invalidateCache(updatedData.identity)
- },
- )
- }
- /**
- * Set the BlobId of the latest profile picture that was sent to this contact.
- *
- * @param blobId The blobId of the latest profile picture sent to this contact, `null` if no
- * profile-picture has been sent, or an empty array if a delete-profile-picture message has
- * been sent.
- *
- * @throws [ModelDeletedException] if model is deleted.
- */
- fun setProfilePictureBlobId(blobId: ByteArray?) {
- this.updateFields(
- methodName = "setProfilePictureBlobId",
- detectChanges = { originalData ->
- !originalData.profilePictureBlobId.contentEquals(
- blobId,
- )
- },
- updateData = { originalData -> originalData.copy(profilePictureBlobId = blobId) },
- updateDatabase = ::updateDatabase,
- onUpdated = ::defaultOnUpdated,
- )
- }
- /**
- * Set whether the contact has been restored or not. After a restore of a backup, every contact
- * is marked as restored to track whether the profile picture must be requested from this
- * contact.
- *
- * @throws [ModelDeletedException] if model is deleted.
- */
- fun setIsRestored(isRestored: Boolean) {
- this.updateFields(
- methodName = "setIsRestored",
- detectChanges = { originalData -> originalData.isRestored != isRestored },
- updateData = { originalData -> originalData.copy(isRestored = isRestored) },
- updateDatabase = ::updateDatabase,
- onUpdated = { updatedData ->
- // No need to notify listeners, this isn't something that will result in a UI change.
- // But keep old-service cache correct:
- deprecatedContactService?.invalidateCache(updatedData.identity)
- },
- )
- }
- /**
- * Update the contact's notification-trigger-policy-override **without** reflecting the change.
- *
- * @throws [ModelDeletedException] if model is deleted.
- */
- fun setNotificationTriggerPolicyOverrideFromSync(notificationTriggerPolicyOverride: Long?) {
- this.updateFields(
- methodName = "setNotificationTriggerPolicyOverrideFromSync",
- detectChanges = { originalData -> originalData.notificationTriggerPolicyOverride != notificationTriggerPolicyOverride },
- updateData = { originalData -> originalData.copy(notificationTriggerPolicyOverride = notificationTriggerPolicyOverride) },
- updateDatabase = ::updateDatabase,
- onUpdated = ::defaultOnUpdated,
- )
- }
- /**
- * Update the contact's notification-trigger-policy-override and reflecting the change.
- *
- * @throws [ModelDeletedException] if model is deleted.
- *
- * @see NotificationTriggerPolicyOverride
- */
- fun setNotificationTriggerPolicyOverrideFromLocal(notificationTriggerPolicyOverride: Long?) {
- if (notificationTriggerPolicyOverride == DEADLINE_INDEFINITE_EXCEPT_MENTIONS) {
- logger.error("Can not set notification-trigger-policy-override value of $notificationTriggerPolicyOverride for contact")
- return
- }
- this.updateFields(
- methodName = "setNotificationTriggerPolicyOverrideFromLocal",
- detectChanges = { originalData -> originalData.notificationTriggerPolicyOverride != notificationTriggerPolicyOverride },
- updateData = { originalData -> originalData.copy(notificationTriggerPolicyOverride = notificationTriggerPolicyOverride) },
- updateDatabase = ::updateDatabase,
- onUpdated = ::defaultOnUpdated,
- reflectUpdateTask = ReflectContactSyncUpdateTask.ReflectNotificationTriggerPolicyOverrideUpdate(
- newNotificationTriggerPolicyOverride = NotificationTriggerPolicyOverride.fromDbValueContact(
- notificationTriggerPolicyOverride,
- ),
- contactIdentity = identity,
- ),
- )
- }
- /**
- * Archive or unarchive the contact.
- *
- * TODO(ANDR-3721): As long as it is possible to mark a contact as pinned outside of the contact model, this method must be used extremely
- * carefully as a contact can never be archived *and* pinned.
- */
- fun setIsArchivedFromLocalOrRemote(isArchived: Boolean) {
- this.updateFields(
- methodName = "setIsArchiveFromLocalOrRemote",
- detectChanges = { originalData -> originalData.isArchived != isArchived },
- updateData = { originalData -> originalData.copy(isArchived = isArchived) },
- updateDatabase = ::updateDatabase,
- onUpdated = ::defaultOnUpdated,
- reflectUpdateTask = ReflectContactSyncUpdateTask.ReflectConversationVisibilityArchiveUpdate(
- isArchived = isArchived,
- contactIdentity = identity,
- ),
- )
- }
- /**
- * Archive or unarchive the contact.
- *
- * TODO(ANDR-3721): As long as it is possible to mark a contact as pinned outside of the contact model, this method must be used extremely
- * carefully as a contact can never be archived *and* pinned.
- */
- fun setIsArchivedFromSync(isArchived: Boolean) {
- this.updateFields(
- methodName = "setIsArchivedFromSync",
- detectChanges = { originalData -> originalData.isArchived != isArchived },
- updateData = { originalData -> originalData.copy(isArchived = isArchived) },
- updateDatabase = ::updateDatabase,
- onUpdated = ::defaultOnUpdated,
- )
- }
- /**
- * Persist the contact's `workLastFullSyncAt` value locally
- */
- fun persistWorkLastFullSyncAt(workLastFullSyncAt: Instant) {
- this.updateFields(
- methodName = "persistWorkLastFullSyncAt",
- detectChanges = { originalData -> originalData.workLastFullSyncAt != workLastFullSyncAt },
- updateData = { originalData -> originalData.copy(workLastFullSyncAt = workLastFullSyncAt) },
- updateDatabase = ::updateDatabase,
- onUpdated = null,
- )
- }
- /**
- * Update the contact's `workLastFullSyncAt` value and **reflect** the change
- */
- fun setWorkLastFullSyncFromLocal(workLastFullSyncAt: Instant) {
- this.updateFields(
- methodName = "setWorkLastFullSyncFromLocal",
- detectChanges = { originalData -> originalData.workLastFullSyncAt != workLastFullSyncAt },
- updateData = { originalData -> originalData.copy(workLastFullSyncAt = workLastFullSyncAt) },
- updateDatabase = ::updateDatabase,
- onUpdated = null,
- reflectUpdateTask = ReflectContactSyncUpdateTask.ReflectWorkLastFullSyncAtUpdate(
- workLastFullSyncAt = workLastFullSyncAt,
- contactIdentity = identity,
- ),
- )
- }
- /**
- * Update all data from database.
- *
- * Note: This method may only be called by the repository, in code that bridges the old models
- * to the new models. All other code does not need to refresh the data, the model's state flow
- * should always be up to date.
- *
- * Note: If the model is marked as deleted, then this will have no effect.
- */
- internal fun refreshFromDb(@Suppress("UNUSED_PARAMETER") token: RepositoryToken) {
- logger.info("Refresh from database")
- synchronized(this) {
- if (mutableData.value == null) {
- logger.warn("Cannot refresh deleted ${this.modelName} from DB")
- return
- }
- val dbContact = databaseBackend.getContactByIdentity(identity) ?: return
- val newData = ContactModelDataFactory.toDataType(dbContact)
- check(newData.identity == identity) {
- "Cannot update contact model with data for different identity: ${newData.identity} != $identity"
- }
- mutableData.value = newData
- }
- }
- private fun updateDatabase(updatedData: ContactModelData) {
- databaseBackend.updateContact(
- dbContact = ContactModelDataFactory.toDbType(updatedData),
- )
- }
- /**
- * Synchronously notify contact change listeners.
- */
- private fun notifyDeprecatedOnModifiedListeners(data: ContactModelData) {
- ListenerManager.contactListeners.handle { it.onModified(data.identity) }
- }
- /**
- * Synchronously notify contact change listeners.
- */
- private fun notifyDeprecatedOnRemovedListeners(identity: IdentityString) {
- ListenerManager.contactListeners.handle { it.onRemoved(identity) }
- }
- private fun defaultOnUpdated(updatedData: ContactModelData) {
- deprecatedContactService?.invalidateCache(updatedData.identity)
- notifyDeprecatedOnModifiedListeners(updatedData)
- }
- }
- internal object ContactModelDataFactory : ModelDataFactory<ContactModelData, DbContact> {
- override fun toDbType(value: ContactModelData): DbContact = DbContact(
- identity = value.identity,
- publicKey = value.publicKey,
- createdAt = value.createdAt,
- firstName = value.firstName,
- lastName = value.lastName,
- nickname = value.nickname,
- colorIndex = value.idColor.colorIndex,
- verificationLevel = value.verificationLevel,
- workVerificationLevel = value.workVerificationLevel,
- identityType = value.identityType,
- acquaintanceLevel = value.acquaintanceLevel,
- activityState = value.activityState,
- syncState = value.syncState,
- featureMask = value.featureMask,
- readReceiptPolicy = value.readReceiptPolicy,
- typingIndicatorPolicy = value.typingIndicatorPolicy,
- isArchived = value.isArchived,
- androidContactLookupKey = value.androidContactLookupInfo.toDatabaseString(),
- localAvatarExpires = value.localAvatarExpires,
- isRestored = value.isRestored,
- profilePictureBlobId = value.profilePictureBlobId,
- jobTitle = value.jobTitle,
- department = value.department,
- notificationTriggerPolicyOverride = value.notificationTriggerPolicyOverride,
- availabilityStatusSet = when (value.availabilityStatus) {
- AvailabilityStatus.None -> null
- is AvailabilityStatus.Set -> value.availabilityStatus
- },
- workLastFullSyncAt = value.workLastFullSyncAt,
- )
- override fun toDataType(value: DbContact): ContactModelData = ContactModelData(
- identity = value.identity,
- publicKey = value.publicKey,
- createdAt = value.createdAt,
- firstName = value.firstName,
- lastName = value.lastName,
- nickname = value.nickname,
- idColor = IdColor(value.colorIndex),
- verificationLevel = value.verificationLevel,
- workVerificationLevel = value.workVerificationLevel,
- identityType = value.identityType,
- acquaintanceLevel = value.acquaintanceLevel,
- activityState = value.activityState,
- syncState = value.syncState,
- featureMask = value.featureMask,
- readReceiptPolicy = value.readReceiptPolicy,
- typingIndicatorPolicy = value.typingIndicatorPolicy,
- isArchived = value.isArchived,
- androidContactLookupInfo = value.androidContactLookupKey.toAndroidContactLookupKey(),
- localAvatarExpires = value.localAvatarExpires,
- isRestored = value.isRestored,
- profilePictureBlobId = value.profilePictureBlobId,
- jobTitle = value.jobTitle,
- department = value.department,
- notificationTriggerPolicyOverride = value.notificationTriggerPolicyOverride,
- availabilityStatus = value.availabilityStatusSet ?: AvailabilityStatus.None,
- workLastFullSyncAt = value.workLastFullSyncAt,
- )
- private fun AndroidContactLookupInfo?.toDatabaseString(): String? = this?.run {
- // Note that we append '/null' on purpose if the contact id is null. This ensures that we parse the lookup key and contact id correctly if the
- // lookup key contains any slashes. When parsing the string, we rely on the last '/' for splitting the lookup key and the contact id.
- "$lookupKey/$contactId"
- }
- private fun String?.toAndroidContactLookupKey(): AndroidContactLookupInfo? = this?.let { androidContactLookupKeyString ->
- AndroidContactLookupInfo(
- lookupKey = androidContactLookupKeyString.substringBeforeLast(delimiter = "/"),
- contactId = androidContactLookupKeyString.substringAfterLast(delimiter = "/", missingDelimiterValue = "").toLongOrNull(),
- )
- }
- }
|