ContactModel.kt 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919
  1. package ch.threema.data.models
  2. import ch.threema.app.ThreemaApplication
  3. import ch.threema.app.managers.CoreServiceManager
  4. import ch.threema.app.managers.ListenerManager
  5. import ch.threema.app.managers.ServiceManager
  6. import ch.threema.app.services.ContactService
  7. import ch.threema.app.services.DeadlineListService.DEADLINE_INDEFINITE_EXCEPT_MENTIONS
  8. import ch.threema.app.tasks.ReflectContactSyncUpdateImmediateTask
  9. import ch.threema.app.tasks.ReflectContactSyncUpdateTask
  10. import ch.threema.app.utils.ContactUtil
  11. import ch.threema.base.utils.getThreemaLogger
  12. import ch.threema.common.toDate
  13. import ch.threema.data.datatypes.AndroidContactLookupInfo
  14. import ch.threema.data.datatypes.AvailabilityStatus
  15. import ch.threema.data.datatypes.IdColor
  16. import ch.threema.data.datatypes.NotificationTriggerPolicyOverride
  17. import ch.threema.data.repositories.RepositoryToken
  18. import ch.threema.data.storage.DatabaseBackend
  19. import ch.threema.data.storage.DbContact
  20. import ch.threema.domain.models.ContactSyncState
  21. import ch.threema.domain.models.IdentityState
  22. import ch.threema.domain.models.IdentityType
  23. import ch.threema.domain.models.ReadReceiptPolicy
  24. import ch.threema.domain.models.TypingIndicatorPolicy
  25. import ch.threema.domain.models.VerificationLevel
  26. import ch.threema.domain.models.WorkVerificationLevel
  27. import ch.threema.domain.protocol.ThreemaFeature
  28. import ch.threema.domain.taskmanager.ActiveTaskCodec
  29. import ch.threema.domain.types.IdentityString
  30. import ch.threema.storage.models.ContactModel.AcquaintanceLevel
  31. import java.time.Instant
  32. import java.util.Date
  33. import kotlinx.coroutines.flow.MutableStateFlow
  34. private val logger = getThreemaLogger("data.ContactModel")
  35. /**
  36. * A contact.
  37. */
  38. class ContactModel(
  39. val identity: IdentityString,
  40. data: ContactModelData,
  41. private val databaseBackend: DatabaseBackend,
  42. coreServiceManager: CoreServiceManager,
  43. ) : BaseModel<ContactModelData, ReflectContactSyncUpdateTask>(
  44. MutableStateFlow(data),
  45. "ContactModel",
  46. coreServiceManager.multiDeviceManager,
  47. coreServiceManager.taskManager,
  48. ) {
  49. init {
  50. require(identity == data.identity) {
  51. "Contact model identity mismatch"
  52. }
  53. require(identity != coreServiceManager.identityStore.getIdentityString()) {
  54. "Cannot create contact model with the identity of the user"
  55. }
  56. }
  57. /**
  58. * We have to make the bridge over to the old ContactService in order
  59. * to keep the new and old caches both correct.
  60. *
  61. * TODO(ANDR-4361): Remove this
  62. */
  63. private val deprecatedContactService: ContactService? by lazy {
  64. val serviceManager: ServiceManager? = ThreemaApplication.getServiceManager()
  65. if (serviceManager == null) {
  66. logger.warn("Tried to get the contactService before the service-manager was created.")
  67. }
  68. serviceManager?.contactService
  69. }
  70. /**
  71. * Update the contact's first and last name.
  72. *
  73. * @throws [ModelDeletedException] if model is deleted.
  74. */
  75. fun setNameFromLocal(firstName: String, lastName: String) {
  76. this.updateFields(
  77. methodName = "setNameFromLocal",
  78. detectChanges = { originalData -> originalData.firstName != firstName || originalData.lastName != lastName },
  79. updateData = { originalData ->
  80. originalData.copy(
  81. firstName = firstName,
  82. lastName = lastName,
  83. )
  84. },
  85. updateDatabase = ::updateDatabase,
  86. onUpdated = ::defaultOnUpdated,
  87. reflectUpdateTask = ReflectContactSyncUpdateTask.ReflectNameUpdate(
  88. newFirstName = firstName,
  89. newLastName = lastName,
  90. contactIdentity = identity,
  91. ),
  92. )
  93. }
  94. /**
  95. * Update the contact's jobTitle
  96. *
  97. * @throws ModelDeletedException if model is deleted.
  98. *
  99. * TODO(ANDR-3611): Reflect change to device group
  100. */
  101. fun setJobTitleFromLocal(jobTitle: String?) {
  102. this.updateFields(
  103. methodName = "setJobTitleFromLocal",
  104. detectChanges = { originalData -> originalData.jobTitle != jobTitle },
  105. updateData = { originalData -> originalData.copy(jobTitle = jobTitle) },
  106. updateDatabase = ::updateDatabase,
  107. onUpdated = ::defaultOnUpdated,
  108. reflectUpdateTask = null,
  109. )
  110. }
  111. /**
  112. * Update the contact's department
  113. *
  114. * @throws ModelDeletedException if model is deleted.
  115. *
  116. * TODO(ANDR-3611): Reflect change to device group
  117. */
  118. fun setDepartmentFromLocal(department: String?) {
  119. this.updateFields(
  120. methodName = "setDepartmentFromLocal",
  121. detectChanges = { originalData -> originalData.department != department },
  122. updateData = { originalData -> originalData.copy(department = department) },
  123. updateDatabase = ::updateDatabase,
  124. onUpdated = ::defaultOnUpdated,
  125. reflectUpdateTask = null,
  126. )
  127. }
  128. /**
  129. * Update the contact's acquaintance level.
  130. *
  131. * @throws [ModelDeletedException] if model is deleted.
  132. */
  133. fun setAcquaintanceLevelFromLocal(acquaintanceLevel: AcquaintanceLevel) {
  134. this.updateFields(
  135. methodName = "setAcquaintanceLevelFromLocal",
  136. detectChanges = { originalData -> originalData.acquaintanceLevel != acquaintanceLevel },
  137. updateData = { originalData -> originalData.copy(acquaintanceLevel = acquaintanceLevel) },
  138. updateDatabase = ::updateDatabase,
  139. onUpdated = { contactModelData ->
  140. deprecatedContactService?.invalidateCache(contactModelData.identity)
  141. when (acquaintanceLevel) {
  142. AcquaintanceLevel.DIRECT -> notifyDeprecatedOnModifiedListeners(contactModelData)
  143. AcquaintanceLevel.GROUP -> notifyDeprecatedOnRemovedListeners(contactModelData.identity)
  144. }
  145. },
  146. reflectUpdateTask = ReflectContactSyncUpdateTask.ReflectAcquaintanceLevelUpdate(
  147. acquaintanceLevel,
  148. identity,
  149. ),
  150. )
  151. }
  152. /**
  153. * Update the contact's verification level.
  154. *
  155. * @throws [ModelDeletedException] if model is deleted.
  156. */
  157. fun setVerificationLevelFromLocal(verificationLevel: VerificationLevel) {
  158. this.updateFields(
  159. methodName = "setVerificationLevelFromLocal",
  160. detectChanges = { originalData -> originalData.verificationLevel != verificationLevel },
  161. updateData = { originalData -> originalData.copy(verificationLevel = verificationLevel) },
  162. updateDatabase = ::updateDatabase,
  163. onUpdated = ::defaultOnUpdated,
  164. reflectUpdateTask = ReflectContactSyncUpdateTask.ReflectVerificationLevelUpdate(
  165. verificationLevel,
  166. identity,
  167. ),
  168. )
  169. }
  170. /**
  171. * Update the contact's work verification level.
  172. *
  173. * @throws [ModelDeletedException] if model is deleted.
  174. */
  175. fun setWorkVerificationLevelFromLocal(workVerificationLevel: WorkVerificationLevel) {
  176. this.updateFields(
  177. methodName = "setWorkVerificationLevelFromLocal",
  178. detectChanges = { originalData -> originalData.workVerificationLevel != workVerificationLevel },
  179. updateData = { originalData -> originalData.copy(workVerificationLevel = workVerificationLevel) },
  180. updateDatabase = ::updateDatabase,
  181. onUpdated = ::defaultOnUpdated,
  182. reflectUpdateTask = ReflectContactSyncUpdateTask.ReflectWorkVerificationLevelUpdate(
  183. workVerificationLevel,
  184. identity,
  185. ),
  186. )
  187. }
  188. /**
  189. * Update the contact's identity type.
  190. *
  191. * @throws [ModelDeletedException] if model is deleted.
  192. */
  193. fun setIdentityTypeFromLocal(identityType: IdentityType) {
  194. this.updateFields(
  195. methodName = "setIdentityTypeFromLocal",
  196. detectChanges = { originalData -> originalData.identityType != identityType },
  197. updateData = { originalData -> originalData.copy(identityType = identityType) },
  198. updateDatabase = ::updateDatabase,
  199. onUpdated = ::defaultOnUpdated,
  200. reflectUpdateTask = ReflectContactSyncUpdateTask.ReflectIdentityTypeUpdate(
  201. identityType,
  202. identity,
  203. ),
  204. )
  205. }
  206. fun setFeatureMaskFromLocal(
  207. featureMask: Long,
  208. ) {
  209. // Warn the user in case there is no forward security support anymore (indicated by a
  210. // feature mask change).
  211. data?.let {
  212. val previousFSSupport = ThreemaFeature.canForwardSecurity(it.featureMaskLong())
  213. val newFSSupport = ThreemaFeature.canForwardSecurity(featureMask)
  214. if (previousFSSupport && !newFSSupport) {
  215. ContactUtil.onForwardSecurityNotSupportedAnymore(this)
  216. }
  217. }
  218. this.updateFields(
  219. methodName = "setFeatureMaskFromLocal",
  220. detectChanges = { originalData -> originalData.featureMask != featureMask.toULong() },
  221. updateData = { originalData -> originalData.copy(featureMask = featureMask.toULong()) },
  222. updateDatabase = ::updateDatabase,
  223. onUpdated = ::defaultOnUpdated,
  224. reflectUpdateTask = ReflectContactSyncUpdateTask.ReflectFeatureMaskUpdate(
  225. featureMask,
  226. identity,
  227. ),
  228. )
  229. }
  230. /**
  231. * Update the contact's `availabilityStatus` value **without** reflecting the change
  232. */
  233. fun setAvailabilityStatusFromSync(availabilityStatus: AvailabilityStatus) {
  234. this.updateFields(
  235. methodName = "setAvailabilityStatusFromSync",
  236. detectChanges = { originalData -> originalData.availabilityStatus != availabilityStatus },
  237. updateData = { originalData -> originalData.copy(availabilityStatus = availabilityStatus) },
  238. updateDatabase = ::updateDatabase,
  239. onUpdated = ::defaultOnUpdated,
  240. )
  241. }
  242. /**
  243. * Persist the contact's `availabilityStatus` locally
  244. */
  245. fun persistAvailabilityStatus(availabilityStatus: AvailabilityStatus) {
  246. this.updateFields(
  247. methodName = "persistAvailabilityStatus",
  248. detectChanges = { originalData -> originalData.availabilityStatus != availabilityStatus },
  249. updateData = { originalData -> originalData.copy(availabilityStatus = availabilityStatus) },
  250. updateDatabase = ::updateDatabase,
  251. onUpdated = ::defaultOnUpdated,
  252. )
  253. }
  254. /**
  255. * Update the contact's first name.
  256. *
  257. * @throws [ModelDeletedException] if model is deleted.
  258. */
  259. fun setFirstNameFromSync(firstName: String) {
  260. this.updateFields(
  261. methodName = "setFirstNameFromSync",
  262. detectChanges = { originalData -> originalData.firstName != firstName },
  263. updateData = { originalData -> originalData.copy(firstName = firstName) },
  264. updateDatabase = ::updateDatabase,
  265. onUpdated = ::defaultOnUpdated,
  266. )
  267. }
  268. /**
  269. * Update the contact's last name.
  270. *
  271. * @throws [ModelDeletedException] if model is deleted.
  272. */
  273. fun setLastNameFromSync(lastName: String) {
  274. this.updateFields(
  275. methodName = "setLastNameFromSync",
  276. detectChanges = { originalData -> originalData.lastName != lastName },
  277. updateData = { originalData -> originalData.copy(lastName = lastName) },
  278. updateDatabase = ::updateDatabase,
  279. onUpdated = ::defaultOnUpdated,
  280. )
  281. }
  282. /**
  283. * Update the contact's public nickname.
  284. *
  285. * @throws [ModelDeletedException] if model is deleted.
  286. */
  287. fun setNicknameFromSync(nickname: String?) {
  288. this.updateFields(
  289. methodName = "setNicknameFromSync",
  290. detectChanges = { originalData -> originalData.nickname != nickname },
  291. updateData = { originalData -> originalData.copy(nickname = nickname) },
  292. updateDatabase = ::updateDatabase,
  293. onUpdated = ::defaultOnUpdated,
  294. )
  295. }
  296. suspend fun setNicknameFromRemote(nickname: String, handle: ActiveTaskCodec) {
  297. logger.debug("Updating nickname of {} to {}", identity, nickname)
  298. // We check whether the nickname is different before trying to reflect it.
  299. val data = ensureNotDeleted("setNicknameFromRemote")
  300. if (data.nickname == nickname) {
  301. return
  302. }
  303. if (multiDeviceManager.isMultiDeviceActive) {
  304. ReflectContactSyncUpdateImmediateTask.ReflectContactNickname(
  305. contactIdentity = identity,
  306. newNickname = nickname,
  307. ).reflect(handle)
  308. }
  309. this.updateFields(
  310. methodName = "setNicknameFromRemote",
  311. detectChanges = { originalData -> originalData.nickname != nickname },
  312. updateData = { originalData -> originalData.copy(nickname = nickname) },
  313. updateDatabase = ::updateDatabase,
  314. onUpdated = ::defaultOnUpdated,
  315. )
  316. }
  317. /**
  318. * Update the contact's verification level.
  319. *
  320. * @throws [ModelDeletedException] if model is deleted.
  321. */
  322. fun setVerificationLevelFromSync(verificationLevel: VerificationLevel) {
  323. this.updateFields(
  324. methodName = "setVerificationLevelFromSync",
  325. detectChanges = { it.verificationLevel != verificationLevel },
  326. updateData = { it.copy(verificationLevel = verificationLevel) },
  327. updateDatabase = ::updateDatabase,
  328. onUpdated = ::defaultOnUpdated,
  329. )
  330. }
  331. /**
  332. * Update the contact's work verification level.
  333. *
  334. * @throws [ModelDeletedException] if model is deleted.
  335. */
  336. fun setWorkVerificationLevelFromSync(workVerificationLevel: WorkVerificationLevel) {
  337. this.updateFields(
  338. methodName = "setWorkVerificationLevelFromSync",
  339. detectChanges = { it.workVerificationLevel != workVerificationLevel },
  340. updateData = { it.copy(workVerificationLevel = workVerificationLevel) },
  341. updateDatabase = ::updateDatabase,
  342. onUpdated = ::defaultOnUpdated,
  343. )
  344. }
  345. /**
  346. * Update the contact's identity type.
  347. *
  348. * @throws [ModelDeletedException] if model is deleted.
  349. */
  350. fun setIdentityTypeFromSync(identityType: IdentityType) {
  351. this.updateFields(
  352. methodName = "setIdentityTypeFromSync",
  353. detectChanges = { it.identityType != identityType },
  354. updateData = { it.copy(identityType = identityType) },
  355. updateDatabase = ::updateDatabase,
  356. onUpdated = ::defaultOnUpdated,
  357. )
  358. }
  359. /**
  360. * Update the contact's acquaintance level.
  361. *
  362. * @throws [ModelDeletedException] if model is deleted.
  363. */
  364. fun setAcquaintanceLevelFromSync(acquaintanceLevel: AcquaintanceLevel) {
  365. this.updateFields(
  366. methodName = "setAcquaintanceLevelFromSync",
  367. detectChanges = { it.acquaintanceLevel != acquaintanceLevel },
  368. updateData = { it.copy(acquaintanceLevel = acquaintanceLevel) },
  369. updateDatabase = ::updateDatabase,
  370. onUpdated = ::defaultOnUpdated,
  371. )
  372. }
  373. /**
  374. * Update the contact's activity state.
  375. *
  376. * @throws [ModelDeletedException] if model is deleted.
  377. */
  378. fun setActivityStateFromSync(activityState: IdentityState) {
  379. this.updateFields(
  380. methodName = "setActivityStateFromSync",
  381. detectChanges = { it.activityState != activityState },
  382. updateData = { it.copy(activityState = activityState) },
  383. updateDatabase = ::updateDatabase,
  384. onUpdated = ::defaultOnUpdated,
  385. )
  386. }
  387. /**
  388. * Update the contact's activity state.
  389. *
  390. * @throws [ModelDeletedException] if model is deleted.
  391. */
  392. fun setActivityStateFromLocal(activityState: IdentityState) {
  393. this.updateFields(
  394. methodName = "setActivityStateFromLocal",
  395. detectChanges = { it.activityState != activityState },
  396. updateData = { it.copy(activityState = activityState) },
  397. updateDatabase = ::updateDatabase,
  398. onUpdated = ::defaultOnUpdated,
  399. reflectUpdateTask = ReflectContactSyncUpdateTask.ReflectActivityStateUpdate(
  400. activityState,
  401. identity,
  402. ),
  403. )
  404. }
  405. /**
  406. * Update the contact's feature mask.
  407. *
  408. * @throws [ModelDeletedException] if model is deleted.
  409. */
  410. fun setFeatureMaskFromSync(featureMask: ULong) {
  411. this.updateFields(
  412. methodName = "setFeatureMaskFromSync",
  413. detectChanges = { it.featureMask != featureMask },
  414. updateData = { it.copy(featureMask = featureMask) },
  415. updateDatabase = ::updateDatabase,
  416. onUpdated = ::defaultOnUpdated,
  417. )
  418. }
  419. /**
  420. * Update the contact's sync state.
  421. *
  422. * @throws [ModelDeletedException] if model is deleted.
  423. */
  424. fun setSyncStateFromSync(syncState: ContactSyncState) {
  425. this.updateFields(
  426. methodName = "setSyncStateFromSync",
  427. detectChanges = { it.syncState != syncState },
  428. updateData = { it.copy(syncState = syncState) },
  429. updateDatabase = ::updateDatabase,
  430. onUpdated = { updatedData ->
  431. // No need to notify listeners, this isn't something that will result in a UI change.
  432. // But keep old-service cache correct:
  433. deprecatedContactService?.invalidateCache(updatedData.identity)
  434. },
  435. )
  436. }
  437. /**
  438. * Update the contact's read receipt policy.
  439. *
  440. * @throws [ModelDeletedException] if model is deleted.
  441. */
  442. fun setReadReceiptPolicyFromSync(readReceiptPolicy: ReadReceiptPolicy) {
  443. this.updateFields(
  444. methodName = "setReadReceiptPolicyFromSync",
  445. detectChanges = { it.readReceiptPolicy != readReceiptPolicy },
  446. updateData = { it.copy(readReceiptPolicy = readReceiptPolicy) },
  447. updateDatabase = ::updateDatabase,
  448. onUpdated = ::defaultOnUpdated,
  449. )
  450. }
  451. /**
  452. * Update the contact's read receipt policy.
  453. *
  454. * @throws ModelDeletedException if model is deleted
  455. */
  456. fun setReadReceiptPolicyFromLocal(readReceiptPolicy: ReadReceiptPolicy) {
  457. this.updateFields(
  458. methodName = "setReadReceiptPolicyFromLocal",
  459. detectChanges = { it.readReceiptPolicy != readReceiptPolicy },
  460. updateData = { it.copy(readReceiptPolicy = readReceiptPolicy) },
  461. updateDatabase = ::updateDatabase,
  462. onUpdated = ::defaultOnUpdated,
  463. reflectUpdateTask = ReflectContactSyncUpdateTask.ReflectReadReceiptPolicyUpdate(
  464. readReceiptPolicy,
  465. identity,
  466. ),
  467. )
  468. }
  469. /**
  470. * Update the contact's typing indicator policy.
  471. *
  472. * @throws [ModelDeletedException] if model is deleted.
  473. */
  474. fun setTypingIndicatorPolicyFromSync(typingIndicatorPolicy: TypingIndicatorPolicy) {
  475. this.updateFields(
  476. methodName = "setTypingIndicatorPolicyFromSync",
  477. detectChanges = { it.typingIndicatorPolicy != typingIndicatorPolicy },
  478. updateData = { it.copy(typingIndicatorPolicy = typingIndicatorPolicy) },
  479. updateDatabase = ::updateDatabase,
  480. onUpdated = ::defaultOnUpdated,
  481. )
  482. }
  483. /**
  484. * Update the contact's typing indicator policy.
  485. *
  486. * @throws [ModelDeletedException] if model is deleted.
  487. */
  488. fun setTypingIndicatorPolicyFromLocal(typingIndicatorPolicy: TypingIndicatorPolicy) {
  489. this.updateFields(
  490. methodName = "setTypingIndicatorPolicyFromLocal",
  491. detectChanges = { it.typingIndicatorPolicy != typingIndicatorPolicy },
  492. updateData = { it.copy(typingIndicatorPolicy = typingIndicatorPolicy) },
  493. updateDatabase = ::updateDatabase,
  494. onUpdated = ::defaultOnUpdated,
  495. reflectUpdateTask = ReflectContactSyncUpdateTask.ReflectTypingIndicatorPolicyUpdate(
  496. typingIndicatorPolicy,
  497. identity,
  498. ),
  499. )
  500. }
  501. /**
  502. * Update or remove the contact's Android contact lookup key.
  503. *
  504. * @throws [ModelDeletedException] if model is deleted.
  505. */
  506. fun setAndroidContactLookupKey(lookupKey: AndroidContactLookupInfo) {
  507. this.updateFields(
  508. methodName = "setAndroidLookupKey",
  509. detectChanges = { originalData -> originalData.androidContactLookupInfo != lookupKey },
  510. updateData = { originalData -> originalData.copy(androidContactLookupInfo = lookupKey) },
  511. updateDatabase = ::updateDatabase,
  512. onUpdated = ::defaultOnUpdated,
  513. )
  514. }
  515. /**
  516. * Unlink the contact from the android contact. This sets the android lookup key to null and
  517. * downgrades the verification level if it is [VerificationLevel.SERVER_VERIFIED]. Note that
  518. * the verification level change is reflected if MD is active.
  519. *
  520. * @throws [ModelDeletedException] if model is deleted.
  521. */
  522. fun removeAndroidContactLink() {
  523. // Remove the android lookup info
  524. this.updateFields(
  525. methodName = "unlinkAndroidContact",
  526. detectChanges = { originalData -> originalData.androidContactLookupInfo != null },
  527. updateData = { originalData -> originalData.copy(androidContactLookupInfo = null) },
  528. updateDatabase = ::updateDatabase,
  529. onUpdated = ::defaultOnUpdated,
  530. )
  531. // Change verification level if it is server verified. Note that we do not use
  532. // setVerificationLevelFromLocal as this must only be done when the verification level is
  533. // server verified.
  534. this.updateFields(
  535. methodName = "unlinkAndroidContact",
  536. detectChanges = { originalData -> originalData.verificationLevel == VerificationLevel.SERVER_VERIFIED },
  537. updateData = { originalData -> originalData.copy(verificationLevel = VerificationLevel.UNVERIFIED) },
  538. updateDatabase = ::updateDatabase,
  539. onUpdated = ::defaultOnUpdated,
  540. reflectUpdateTask = ReflectContactSyncUpdateTask.ReflectVerificationLevelUpdate(
  541. VerificationLevel.UNVERIFIED,
  542. identity,
  543. ),
  544. )
  545. }
  546. /**
  547. * Set the id color to the given value. Note that normally it isn't necessary to set this manually.
  548. */
  549. fun setIdColor(idColor: IdColor) {
  550. this.updateFields(
  551. methodName = "setIdColor",
  552. detectChanges = { originalData -> originalData.idColor != idColor },
  553. updateData = { originalData -> originalData.copy(idColor = idColor) },
  554. updateDatabase = ::updateDatabase,
  555. onUpdated = null,
  556. )
  557. }
  558. /**
  559. * Update or remove the contact's local avatar expiration date.
  560. *
  561. * @throws [ModelDeletedException] if model is deleted.
  562. */
  563. fun setLocalAvatarExpires(expiresAt: Instant?) {
  564. setLocalAvatarExpires(expiresAt?.toDate())
  565. }
  566. private fun setLocalAvatarExpires(expiresAt: Date?) {
  567. this.updateFields(
  568. methodName = "setLocalAvatarExpires",
  569. detectChanges = { originalData -> originalData.localAvatarExpires != expiresAt },
  570. updateData = { originalData -> originalData.copy(localAvatarExpires = expiresAt) },
  571. updateDatabase = ::updateDatabase,
  572. onUpdated = { updatedData ->
  573. // No need to notify listeners, this isn't something that will result in a UI change.
  574. // But keep old-service cache correct:
  575. deprecatedContactService?.invalidateCache(updatedData.identity)
  576. },
  577. )
  578. }
  579. /**
  580. * Clear the "isRestored" flag on the contact.
  581. *
  582. * This should be called once the post-restore sync steps (e.g. profile picture request)
  583. * have been completed.
  584. *
  585. * @throws [ModelDeletedException] if model is deleted.
  586. */
  587. fun clearIsRestored() {
  588. this.updateFields(
  589. methodName = "clearIsRestored",
  590. detectChanges = { originalData -> originalData.isRestored },
  591. updateData = { originalData -> originalData.copy(isRestored = false) },
  592. updateDatabase = ::updateDatabase,
  593. onUpdated = { updatedData ->
  594. // No need to notify listeners, this isn't something that will result in a UI change.
  595. // But keep old-service cache correct:
  596. deprecatedContactService?.invalidateCache(updatedData.identity)
  597. },
  598. )
  599. }
  600. /**
  601. * Set the BlobId of the latest profile picture that was sent to this contact.
  602. *
  603. * @param blobId The blobId of the latest profile picture sent to this contact, `null` if no
  604. * profile-picture has been sent, or an empty array if a delete-profile-picture message has
  605. * been sent.
  606. *
  607. * @throws [ModelDeletedException] if model is deleted.
  608. */
  609. fun setProfilePictureBlobId(blobId: ByteArray?) {
  610. this.updateFields(
  611. methodName = "setProfilePictureBlobId",
  612. detectChanges = { originalData ->
  613. !originalData.profilePictureBlobId.contentEquals(
  614. blobId,
  615. )
  616. },
  617. updateData = { originalData -> originalData.copy(profilePictureBlobId = blobId) },
  618. updateDatabase = ::updateDatabase,
  619. onUpdated = ::defaultOnUpdated,
  620. )
  621. }
  622. /**
  623. * Set whether the contact has been restored or not. After a restore of a backup, every contact
  624. * is marked as restored to track whether the profile picture must be requested from this
  625. * contact.
  626. *
  627. * @throws [ModelDeletedException] if model is deleted.
  628. */
  629. fun setIsRestored(isRestored: Boolean) {
  630. this.updateFields(
  631. methodName = "setIsRestored",
  632. detectChanges = { originalData -> originalData.isRestored != isRestored },
  633. updateData = { originalData -> originalData.copy(isRestored = isRestored) },
  634. updateDatabase = ::updateDatabase,
  635. onUpdated = { updatedData ->
  636. // No need to notify listeners, this isn't something that will result in a UI change.
  637. // But keep old-service cache correct:
  638. deprecatedContactService?.invalidateCache(updatedData.identity)
  639. },
  640. )
  641. }
  642. /**
  643. * Update the contact's notification-trigger-policy-override **without** reflecting the change.
  644. *
  645. * @throws [ModelDeletedException] if model is deleted.
  646. */
  647. fun setNotificationTriggerPolicyOverrideFromSync(notificationTriggerPolicyOverride: Long?) {
  648. this.updateFields(
  649. methodName = "setNotificationTriggerPolicyOverrideFromSync",
  650. detectChanges = { originalData -> originalData.notificationTriggerPolicyOverride != notificationTriggerPolicyOverride },
  651. updateData = { originalData -> originalData.copy(notificationTriggerPolicyOverride = notificationTriggerPolicyOverride) },
  652. updateDatabase = ::updateDatabase,
  653. onUpdated = ::defaultOnUpdated,
  654. )
  655. }
  656. /**
  657. * Update the contact's notification-trigger-policy-override and reflecting the change.
  658. *
  659. * @throws [ModelDeletedException] if model is deleted.
  660. *
  661. * @see NotificationTriggerPolicyOverride
  662. */
  663. fun setNotificationTriggerPolicyOverrideFromLocal(notificationTriggerPolicyOverride: Long?) {
  664. if (notificationTriggerPolicyOverride == DEADLINE_INDEFINITE_EXCEPT_MENTIONS) {
  665. logger.error("Can not set notification-trigger-policy-override value of $notificationTriggerPolicyOverride for contact")
  666. return
  667. }
  668. this.updateFields(
  669. methodName = "setNotificationTriggerPolicyOverrideFromLocal",
  670. detectChanges = { originalData -> originalData.notificationTriggerPolicyOverride != notificationTriggerPolicyOverride },
  671. updateData = { originalData -> originalData.copy(notificationTriggerPolicyOverride = notificationTriggerPolicyOverride) },
  672. updateDatabase = ::updateDatabase,
  673. onUpdated = ::defaultOnUpdated,
  674. reflectUpdateTask = ReflectContactSyncUpdateTask.ReflectNotificationTriggerPolicyOverrideUpdate(
  675. newNotificationTriggerPolicyOverride = NotificationTriggerPolicyOverride.fromDbValueContact(
  676. notificationTriggerPolicyOverride,
  677. ),
  678. contactIdentity = identity,
  679. ),
  680. )
  681. }
  682. /**
  683. * Archive or unarchive the contact.
  684. *
  685. * 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
  686. * carefully as a contact can never be archived *and* pinned.
  687. */
  688. fun setIsArchivedFromLocalOrRemote(isArchived: Boolean) {
  689. this.updateFields(
  690. methodName = "setIsArchiveFromLocalOrRemote",
  691. detectChanges = { originalData -> originalData.isArchived != isArchived },
  692. updateData = { originalData -> originalData.copy(isArchived = isArchived) },
  693. updateDatabase = ::updateDatabase,
  694. onUpdated = ::defaultOnUpdated,
  695. reflectUpdateTask = ReflectContactSyncUpdateTask.ReflectConversationVisibilityArchiveUpdate(
  696. isArchived = isArchived,
  697. contactIdentity = identity,
  698. ),
  699. )
  700. }
  701. /**
  702. * Archive or unarchive the contact.
  703. *
  704. * 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
  705. * carefully as a contact can never be archived *and* pinned.
  706. */
  707. fun setIsArchivedFromSync(isArchived: Boolean) {
  708. this.updateFields(
  709. methodName = "setIsArchivedFromSync",
  710. detectChanges = { originalData -> originalData.isArchived != isArchived },
  711. updateData = { originalData -> originalData.copy(isArchived = isArchived) },
  712. updateDatabase = ::updateDatabase,
  713. onUpdated = ::defaultOnUpdated,
  714. )
  715. }
  716. /**
  717. * Persist the contact's `workLastFullSyncAt` value locally
  718. */
  719. fun persistWorkLastFullSyncAt(workLastFullSyncAt: Instant) {
  720. this.updateFields(
  721. methodName = "persistWorkLastFullSyncAt",
  722. detectChanges = { originalData -> originalData.workLastFullSyncAt != workLastFullSyncAt },
  723. updateData = { originalData -> originalData.copy(workLastFullSyncAt = workLastFullSyncAt) },
  724. updateDatabase = ::updateDatabase,
  725. onUpdated = null,
  726. )
  727. }
  728. /**
  729. * Update the contact's `workLastFullSyncAt` value and **reflect** the change
  730. */
  731. fun setWorkLastFullSyncFromLocal(workLastFullSyncAt: Instant) {
  732. this.updateFields(
  733. methodName = "setWorkLastFullSyncFromLocal",
  734. detectChanges = { originalData -> originalData.workLastFullSyncAt != workLastFullSyncAt },
  735. updateData = { originalData -> originalData.copy(workLastFullSyncAt = workLastFullSyncAt) },
  736. updateDatabase = ::updateDatabase,
  737. onUpdated = null,
  738. reflectUpdateTask = ReflectContactSyncUpdateTask.ReflectWorkLastFullSyncAtUpdate(
  739. workLastFullSyncAt = workLastFullSyncAt,
  740. contactIdentity = identity,
  741. ),
  742. )
  743. }
  744. /**
  745. * Update all data from database.
  746. *
  747. * Note: This method may only be called by the repository, in code that bridges the old models
  748. * to the new models. All other code does not need to refresh the data, the model's state flow
  749. * should always be up to date.
  750. *
  751. * Note: If the model is marked as deleted, then this will have no effect.
  752. */
  753. internal fun refreshFromDb(@Suppress("UNUSED_PARAMETER") token: RepositoryToken) {
  754. logger.info("Refresh from database")
  755. synchronized(this) {
  756. if (mutableData.value == null) {
  757. logger.warn("Cannot refresh deleted ${this.modelName} from DB")
  758. return
  759. }
  760. val dbContact = databaseBackend.getContactByIdentity(identity) ?: return
  761. val newData = ContactModelDataFactory.toDataType(dbContact)
  762. check(newData.identity == identity) {
  763. "Cannot update contact model with data for different identity: ${newData.identity} != $identity"
  764. }
  765. mutableData.value = newData
  766. }
  767. }
  768. private fun updateDatabase(updatedData: ContactModelData) {
  769. databaseBackend.updateContact(
  770. dbContact = ContactModelDataFactory.toDbType(updatedData),
  771. )
  772. }
  773. /**
  774. * Synchronously notify contact change listeners.
  775. */
  776. private fun notifyDeprecatedOnModifiedListeners(data: ContactModelData) {
  777. ListenerManager.contactListeners.handle { it.onModified(data.identity) }
  778. }
  779. /**
  780. * Synchronously notify contact change listeners.
  781. */
  782. private fun notifyDeprecatedOnRemovedListeners(identity: IdentityString) {
  783. ListenerManager.contactListeners.handle { it.onRemoved(identity) }
  784. }
  785. private fun defaultOnUpdated(updatedData: ContactModelData) {
  786. deprecatedContactService?.invalidateCache(updatedData.identity)
  787. notifyDeprecatedOnModifiedListeners(updatedData)
  788. }
  789. }
  790. internal object ContactModelDataFactory : ModelDataFactory<ContactModelData, DbContact> {
  791. override fun toDbType(value: ContactModelData): DbContact = DbContact(
  792. identity = value.identity,
  793. publicKey = value.publicKey,
  794. createdAt = value.createdAt,
  795. firstName = value.firstName,
  796. lastName = value.lastName,
  797. nickname = value.nickname,
  798. colorIndex = value.idColor.colorIndex,
  799. verificationLevel = value.verificationLevel,
  800. workVerificationLevel = value.workVerificationLevel,
  801. identityType = value.identityType,
  802. acquaintanceLevel = value.acquaintanceLevel,
  803. activityState = value.activityState,
  804. syncState = value.syncState,
  805. featureMask = value.featureMask,
  806. readReceiptPolicy = value.readReceiptPolicy,
  807. typingIndicatorPolicy = value.typingIndicatorPolicy,
  808. isArchived = value.isArchived,
  809. androidContactLookupKey = value.androidContactLookupInfo.toDatabaseString(),
  810. localAvatarExpires = value.localAvatarExpires,
  811. isRestored = value.isRestored,
  812. profilePictureBlobId = value.profilePictureBlobId,
  813. jobTitle = value.jobTitle,
  814. department = value.department,
  815. notificationTriggerPolicyOverride = value.notificationTriggerPolicyOverride,
  816. availabilityStatusSet = when (value.availabilityStatus) {
  817. AvailabilityStatus.None -> null
  818. is AvailabilityStatus.Set -> value.availabilityStatus
  819. },
  820. workLastFullSyncAt = value.workLastFullSyncAt,
  821. )
  822. override fun toDataType(value: DbContact): ContactModelData = ContactModelData(
  823. identity = value.identity,
  824. publicKey = value.publicKey,
  825. createdAt = value.createdAt,
  826. firstName = value.firstName,
  827. lastName = value.lastName,
  828. nickname = value.nickname,
  829. idColor = IdColor(value.colorIndex),
  830. verificationLevel = value.verificationLevel,
  831. workVerificationLevel = value.workVerificationLevel,
  832. identityType = value.identityType,
  833. acquaintanceLevel = value.acquaintanceLevel,
  834. activityState = value.activityState,
  835. syncState = value.syncState,
  836. featureMask = value.featureMask,
  837. readReceiptPolicy = value.readReceiptPolicy,
  838. typingIndicatorPolicy = value.typingIndicatorPolicy,
  839. isArchived = value.isArchived,
  840. androidContactLookupInfo = value.androidContactLookupKey.toAndroidContactLookupKey(),
  841. localAvatarExpires = value.localAvatarExpires,
  842. isRestored = value.isRestored,
  843. profilePictureBlobId = value.profilePictureBlobId,
  844. jobTitle = value.jobTitle,
  845. department = value.department,
  846. notificationTriggerPolicyOverride = value.notificationTriggerPolicyOverride,
  847. availabilityStatus = value.availabilityStatusSet ?: AvailabilityStatus.None,
  848. workLastFullSyncAt = value.workLastFullSyncAt,
  849. )
  850. private fun AndroidContactLookupInfo?.toDatabaseString(): String? = this?.run {
  851. // 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
  852. // lookup key contains any slashes. When parsing the string, we rely on the last '/' for splitting the lookup key and the contact id.
  853. "$lookupKey/$contactId"
  854. }
  855. private fun String?.toAndroidContactLookupKey(): AndroidContactLookupInfo? = this?.let { androidContactLookupKeyString ->
  856. AndroidContactLookupInfo(
  857. lookupKey = androidContactLookupKeyString.substringBeforeLast(delimiter = "/"),
  858. contactId = androidContactLookupKeyString.substringAfterLast(delimiter = "/", missingDelimiterValue = "").toLongOrNull(),
  859. )
  860. }
  861. }