mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-23 20:48:43 +00:00
Add notification profiles to storage service.
This commit is contained in:
committed by
Cody Henthorne
parent
07d961fc09
commit
e3ee3d3dba
@@ -26,6 +26,7 @@ import org.whispersystems.signalservice.api.storage.StorageId
|
|||||||
import org.whispersystems.signalservice.api.util.UuidUtil
|
import org.whispersystems.signalservice.api.util.UuidUtil
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import org.whispersystems.signalservice.internal.storage.protos.ChatFolderRecord as RemoteChatFolderRecord
|
import org.whispersystems.signalservice.internal.storage.protos.ChatFolderRecord as RemoteChatFolderRecord
|
||||||
|
import org.whispersystems.signalservice.internal.storage.protos.Recipient as RemoteRecipient
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
class ChatFolderTablesTest {
|
class ChatFolderTablesTest {
|
||||||
@@ -168,11 +169,11 @@ class ChatFolderTablesTest {
|
|||||||
folderType = RemoteChatFolderRecord.FolderType.CUSTOM,
|
folderType = RemoteChatFolderRecord.FolderType.CUSTOM,
|
||||||
deletedAtTimestampMs = folder1.deletedTimestampMs,
|
deletedAtTimestampMs = folder1.deletedTimestampMs,
|
||||||
includedRecipients = listOf(
|
includedRecipients = listOf(
|
||||||
RemoteChatFolderRecord.Recipient(RemoteChatFolderRecord.Recipient.Contact(Recipient.resolved(alice).serviceId.get().toString())),
|
RemoteRecipient(RemoteRecipient.Contact(Recipient.resolved(alice).serviceId.get().toString())),
|
||||||
RemoteChatFolderRecord.Recipient(RemoteChatFolderRecord.Recipient.Contact(Recipient.resolved(bob).serviceId.get().toString()))
|
RemoteRecipient(RemoteRecipient.Contact(Recipient.resolved(bob).serviceId.get().toString()))
|
||||||
),
|
),
|
||||||
excludedRecipients = listOf(
|
excludedRecipients = listOf(
|
||||||
RemoteChatFolderRecord.Recipient(RemoteChatFolderRecord.Recipient.Contact(Recipient.resolved(charlie).serviceId.get().toString()))
|
RemoteRecipient(RemoteRecipient.Contact(Recipient.resolved(charlie).serviceId.get().toString()))
|
||||||
)
|
)
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,174 @@
|
|||||||
|
package org.thoughtcrime.securesms.database
|
||||||
|
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import assertk.assertThat
|
||||||
|
import assertk.assertions.containsExactlyInAnyOrder
|
||||||
|
import assertk.assertions.isEmpty
|
||||||
|
import assertk.assertions.isEqualTo
|
||||||
|
import assertk.assertions.isFalse
|
||||||
|
import okio.ByteString.Companion.toByteString
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertNotEquals
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.signal.core.util.deleteAll
|
||||||
|
import org.thoughtcrime.securesms.conversation.colors.AvatarColor
|
||||||
|
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile
|
||||||
|
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfileId
|
||||||
|
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfileSchedule
|
||||||
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
|
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||||
|
import org.thoughtcrime.securesms.storage.StorageSyncHelper
|
||||||
|
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||||
|
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||||
|
import org.whispersystems.signalservice.api.storage.SignalNotificationProfileRecord
|
||||||
|
import org.whispersystems.signalservice.api.storage.StorageId
|
||||||
|
import org.whispersystems.signalservice.api.util.UuidUtil
|
||||||
|
import java.time.DayOfWeek
|
||||||
|
import java.util.UUID
|
||||||
|
import org.whispersystems.signalservice.internal.storage.protos.NotificationProfile as RemoteNotificationProfile
|
||||||
|
import org.whispersystems.signalservice.internal.storage.protos.Recipient as RemoteRecipient
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class NotificationProfileTablesTest {
|
||||||
|
|
||||||
|
@get:Rule
|
||||||
|
val harness = SignalActivityRule()
|
||||||
|
|
||||||
|
private lateinit var alice: RecipientId
|
||||||
|
private lateinit var profile1: NotificationProfile
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
alice = SignalDatabase.recipients.getOrInsertFromServiceId(ACI.from(UUID.randomUUID()))
|
||||||
|
|
||||||
|
profile1 = NotificationProfile(
|
||||||
|
id = 1,
|
||||||
|
name = "profile1",
|
||||||
|
emoji = "",
|
||||||
|
createdAt = 1000L,
|
||||||
|
schedule = NotificationProfileSchedule(id = 1),
|
||||||
|
allowedMembers = setOf(alice),
|
||||||
|
notificationProfileId = NotificationProfileId.generate(),
|
||||||
|
deletedTimestampMs = 0,
|
||||||
|
storageServiceId = StorageId.forNotificationProfile(byteArrayOf(1, 2, 3))
|
||||||
|
)
|
||||||
|
|
||||||
|
SignalDatabase.notificationProfiles.writableDatabase.deleteAll(NotificationProfileTables.NotificationProfileTable.TABLE_NAME)
|
||||||
|
SignalDatabase.notificationProfiles.writableDatabase.deleteAll(NotificationProfileTables.NotificationProfileScheduleTable.TABLE_NAME)
|
||||||
|
SignalDatabase.notificationProfiles.writableDatabase.deleteAll(NotificationProfileTables.NotificationProfileAllowedMembersTable.TABLE_NAME)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun givenARemoteProfile_whenIInsertLocally_thenIExpectAListWithThatProfile() {
|
||||||
|
val remoteRecord =
|
||||||
|
SignalNotificationProfileRecord(
|
||||||
|
profile1.storageServiceId!!,
|
||||||
|
RemoteNotificationProfile(
|
||||||
|
id = UuidUtil.toByteArray(profile1.notificationProfileId.uuid).toByteString(),
|
||||||
|
name = "profile1",
|
||||||
|
emoji = "",
|
||||||
|
color = profile1.color.colorInt(),
|
||||||
|
createdAtMs = 1000L,
|
||||||
|
allowedMembers = listOf(RemoteRecipient(RemoteRecipient.Contact(Recipient.resolved(alice).serviceId.get().toString()))),
|
||||||
|
allowAllMentions = false,
|
||||||
|
allowAllCalls = true,
|
||||||
|
scheduleEnabled = false,
|
||||||
|
scheduleStartTime = 900,
|
||||||
|
scheduleEndTime = 1700,
|
||||||
|
scheduleDaysEnabled = emptyList(),
|
||||||
|
deletedAtTimestampMs = 0
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
SignalDatabase.notificationProfiles.insertNotificationProfileFromStorageSync(remoteRecord)
|
||||||
|
val actualProfiles = SignalDatabase.notificationProfiles.getProfiles()
|
||||||
|
|
||||||
|
assertEquals(listOf(profile1), actualProfiles)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun givenAProfile_whenIDeleteIt_thenIExpectAnEmptyList() {
|
||||||
|
val profile: NotificationProfile = SignalDatabase.notificationProfiles.createProfile(
|
||||||
|
name = "Profile",
|
||||||
|
emoji = "avatar",
|
||||||
|
color = AvatarColor.A210,
|
||||||
|
createdAt = 1000L
|
||||||
|
).profile
|
||||||
|
|
||||||
|
SignalDatabase.notificationProfiles.deleteProfile(profile.id)
|
||||||
|
|
||||||
|
assertThat(SignalDatabase.notificationProfiles.getProfiles()).isEmpty()
|
||||||
|
assertThat(SignalDatabase.notificationProfiles.getProfile(profile.id))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun givenADeletedProfile_whenIGetIt_thenIExpectItToStillHaveASchedule() {
|
||||||
|
val profile: NotificationProfile = SignalDatabase.notificationProfiles.createProfile(
|
||||||
|
name = "Profile",
|
||||||
|
emoji = "avatar",
|
||||||
|
color = AvatarColor.A210,
|
||||||
|
createdAt = 1000L
|
||||||
|
).profile
|
||||||
|
|
||||||
|
SignalDatabase.notificationProfiles.deleteProfile(profile.id)
|
||||||
|
|
||||||
|
val deletedProfile = SignalDatabase.notificationProfiles.getProfile(profile.id)!!
|
||||||
|
assertThat(deletedProfile.schedule.enabled).isFalse()
|
||||||
|
assertThat(deletedProfile.schedule.start).isEqualTo(900)
|
||||||
|
assertThat(deletedProfile.schedule.end).isEqualTo(1700)
|
||||||
|
assertThat(deletedProfile.schedule.daysEnabled, "Contains correct default days")
|
||||||
|
.containsExactlyInAnyOrder(DayOfWeek.MONDAY, DayOfWeek.TUESDAY, DayOfWeek.WEDNESDAY, DayOfWeek.THURSDAY, DayOfWeek.FRIDAY)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun givenNotificationProfiles_whenIUpdateTheirStorageSyncIds_thenIExpectAnUpdatedList() {
|
||||||
|
SignalDatabase.notificationProfiles.createProfile(
|
||||||
|
name = "Profile1",
|
||||||
|
emoji = "avatar",
|
||||||
|
color = AvatarColor.A210,
|
||||||
|
createdAt = 1000L
|
||||||
|
)
|
||||||
|
SignalDatabase.notificationProfiles.createProfile(
|
||||||
|
name = "Profile2",
|
||||||
|
emoji = "avatar",
|
||||||
|
color = AvatarColor.A210,
|
||||||
|
createdAt = 2000L
|
||||||
|
)
|
||||||
|
|
||||||
|
val existingMap = SignalDatabase.notificationProfiles.getStorageSyncIdsMap()
|
||||||
|
existingMap.forEach { (id, _) ->
|
||||||
|
SignalDatabase.notificationProfiles.applyStorageIdUpdate(id, StorageId.forNotificationProfile(StorageSyncHelper.generateKey()))
|
||||||
|
}
|
||||||
|
val updatedMap = SignalDatabase.notificationProfiles.getStorageSyncIdsMap()
|
||||||
|
|
||||||
|
existingMap.forEach { (id, storageId) ->
|
||||||
|
assertNotEquals(storageId, updatedMap[id])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun givenAProfileDeletedOver30Days_whenICleanUp_thenIExpectItToNotHaveAStorageId() {
|
||||||
|
val remoteRecord =
|
||||||
|
SignalNotificationProfileRecord(
|
||||||
|
profile1.storageServiceId!!,
|
||||||
|
RemoteNotificationProfile(
|
||||||
|
id = UuidUtil.toByteArray(profile1.notificationProfileId.uuid).toByteString(),
|
||||||
|
name = "profile1",
|
||||||
|
emoji = "",
|
||||||
|
color = profile1.color.colorInt(),
|
||||||
|
createdAtMs = 1000L,
|
||||||
|
deletedAtTimestampMs = 1000L
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
SignalDatabase.notificationProfiles.insertNotificationProfileFromStorageSync(remoteRecord)
|
||||||
|
SignalDatabase.notificationProfiles.removeStorageIdsFromOldDeletedProfiles(System.currentTimeMillis())
|
||||||
|
assertThat(SignalDatabase.notificationProfiles.getStorageSyncIds()).isEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val NotificationProfileTables.NotificationProfileChangeResult.profile: NotificationProfile
|
||||||
|
get() = (this as NotificationProfileTables.NotificationProfileChangeResult.Success).notificationProfile
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
package org.thoughtcrime.securesms.backup.v2.processor
|
package org.thoughtcrime.securesms.backup.v2.processor
|
||||||
|
|
||||||
import okio.ByteString.Companion.toByteString
|
import okio.ByteString.Companion.toByteString
|
||||||
|
import org.signal.core.util.Base64
|
||||||
import org.signal.core.util.insertInto
|
import org.signal.core.util.insertInto
|
||||||
import org.signal.core.util.logging.Log
|
import org.signal.core.util.logging.Log
|
||||||
import org.signal.core.util.toInt
|
import org.signal.core.util.toInt
|
||||||
@@ -22,6 +23,7 @@ import org.thoughtcrime.securesms.database.SignalDatabase
|
|||||||
import org.thoughtcrime.securesms.database.serialize
|
import org.thoughtcrime.securesms.database.serialize
|
||||||
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile
|
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||||
|
import org.thoughtcrime.securesms.storage.StorageSyncHelper
|
||||||
import org.whispersystems.signalservice.api.util.UuidUtil
|
import org.whispersystems.signalservice.api.util.UuidUtil
|
||||||
import java.time.DayOfWeek
|
import java.time.DayOfWeek
|
||||||
import org.thoughtcrime.securesms.backup.v2.proto.NotificationProfile as NotificationProfileProto
|
import org.thoughtcrime.securesms.backup.v2.proto.NotificationProfile as NotificationProfileProto
|
||||||
@@ -59,7 +61,8 @@ object NotificationProfileProcessor {
|
|||||||
NotificationProfileTable.CREATED_AT to profile.createdAtMs,
|
NotificationProfileTable.CREATED_AT to profile.createdAtMs,
|
||||||
NotificationProfileTable.ALLOW_ALL_CALLS to profile.allowAllCalls.toInt(),
|
NotificationProfileTable.ALLOW_ALL_CALLS to profile.allowAllCalls.toInt(),
|
||||||
NotificationProfileTable.ALLOW_ALL_MENTIONS to profile.allowAllMentions.toInt(),
|
NotificationProfileTable.ALLOW_ALL_MENTIONS to profile.allowAllMentions.toInt(),
|
||||||
NotificationProfileTable.NOTIFICATION_PROFILE_ID to notificationProfileUuid.toString()
|
NotificationProfileTable.NOTIFICATION_PROFILE_ID to notificationProfileUuid.toString(),
|
||||||
|
NotificationProfileTable.STORAGE_SERVICE_ID to Base64.encodeWithPadding(StorageSyncHelper.generateKey())
|
||||||
)
|
)
|
||||||
.run()
|
.run()
|
||||||
|
|
||||||
|
|||||||
@@ -338,6 +338,12 @@ private fun StorageRecordRow(record: SignalStorageRecord) {
|
|||||||
ManifestItemRow("ID", Hex.toStringCondensed(record.id.raw))
|
ManifestItemRow("ID", Hex.toStringCondensed(record.id.raw))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
record.proto.notificationProfile != null -> {
|
||||||
|
Column {
|
||||||
|
Text("Notification Profile", fontWeight = FontWeight.Bold)
|
||||||
|
ManifestItemRow("ID", Hex.toStringCondensed(record.id.raw))
|
||||||
|
}
|
||||||
|
}
|
||||||
else -> {
|
else -> {
|
||||||
Column {
|
Column {
|
||||||
Text("Unknown!")
|
Text("Unknown!")
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ class EditNotificationProfileScheduleViewModel(
|
|||||||
repository.updateSchedule(schedule)
|
repository.updateSchedule(schedule)
|
||||||
.toSingleDefault(SaveScheduleResult.Success)
|
.toSingleDefault(SaveScheduleResult.Success)
|
||||||
.flatMap { r ->
|
.flatMap { r ->
|
||||||
|
repository.scheduleNotificationProfileSync(profileId)
|
||||||
if (schedule.enabled && schedule.coversTime(System.currentTimeMillis())) {
|
if (schedule.enabled && schedule.coversTime(System.currentTimeMillis())) {
|
||||||
repository.manuallyEnableProfileForSchedule(profileId, schedule)
|
repository.manuallyEnableProfileForSchedule(profileId, schedule)
|
||||||
.toSingleDefault(r)
|
.toSingleDefault(r)
|
||||||
|
|||||||
@@ -16,7 +16,9 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore
|
|||||||
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile
|
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile
|
||||||
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfileSchedule
|
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfileSchedule
|
||||||
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfiles
|
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfiles
|
||||||
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||||
|
import org.thoughtcrime.securesms.storage.StorageSyncHelper
|
||||||
import org.thoughtcrime.securesms.util.toLocalDateTime
|
import org.thoughtcrime.securesms.util.toLocalDateTime
|
||||||
import org.thoughtcrime.securesms.util.toMillis
|
import org.thoughtcrime.securesms.util.toMillis
|
||||||
|
|
||||||
@@ -56,36 +58,43 @@ class NotificationProfilesRepository {
|
|||||||
|
|
||||||
fun createProfile(name: String, selectedEmoji: String): Single<NotificationProfileTables.NotificationProfileChangeResult> {
|
fun createProfile(name: String, selectedEmoji: String): Single<NotificationProfileTables.NotificationProfileChangeResult> {
|
||||||
return Single.fromCallable { database.createProfile(name = name, emoji = selectedEmoji, color = AvatarColor.random(), createdAt = System.currentTimeMillis()) }
|
return Single.fromCallable { database.createProfile(name = name, emoji = selectedEmoji, color = AvatarColor.random(), createdAt = System.currentTimeMillis()) }
|
||||||
|
.doOnSuccess { StorageSyncHelper.scheduleSyncForDataChange() }
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateProfile(profileId: Long, name: String, selectedEmoji: String): Single<NotificationProfileTables.NotificationProfileChangeResult> {
|
fun updateProfile(profileId: Long, name: String, selectedEmoji: String): Single<NotificationProfileTables.NotificationProfileChangeResult> {
|
||||||
return Single.fromCallable { database.updateProfile(profileId = profileId, name = name, emoji = selectedEmoji) }
|
return Single.fromCallable { database.updateProfile(profileId = profileId, name = name, emoji = selectedEmoji) }
|
||||||
|
.doOnSuccess { scheduleNotificationProfileSync(profileId) }
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateProfile(profile: NotificationProfile): Single<NotificationProfileTables.NotificationProfileChangeResult> {
|
fun updateProfile(profile: NotificationProfile): Single<NotificationProfileTables.NotificationProfileChangeResult> {
|
||||||
return Single.fromCallable { database.updateProfile(profile) }
|
return Single.fromCallable { database.updateProfile(profile) }
|
||||||
|
.doOnSuccess { scheduleNotificationProfileSync(profile.id) }
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateAllowedMembers(profileId: Long, recipients: Set<RecipientId>): Single<NotificationProfile> {
|
fun updateAllowedMembers(profileId: Long, recipients: Set<RecipientId>): Single<NotificationProfile> {
|
||||||
return Single.fromCallable { database.setAllowedRecipients(profileId, recipients) }
|
return Single.fromCallable { database.setAllowedRecipients(profileId, recipients) }
|
||||||
|
.doOnSuccess { scheduleNotificationProfileSync(profileId) }
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeMember(profileId: Long, recipientId: RecipientId): Single<NotificationProfile> {
|
fun removeMember(profileId: Long, recipientId: RecipientId): Single<NotificationProfile> {
|
||||||
return Single.fromCallable { database.removeAllowedRecipient(profileId, recipientId) }
|
return Single.fromCallable { database.removeAllowedRecipient(profileId, recipientId) }
|
||||||
|
.doOnSuccess { scheduleNotificationProfileSync(profileId) }
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addMember(profileId: Long, recipientId: RecipientId): Single<NotificationProfile> {
|
fun addMember(profileId: Long, recipientId: RecipientId): Single<NotificationProfile> {
|
||||||
return Single.fromCallable { database.addAllowedRecipient(profileId, recipientId) }
|
return Single.fromCallable { database.addAllowedRecipient(profileId, recipientId) }
|
||||||
|
.doOnSuccess { scheduleNotificationProfileSync(profileId) }
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteProfile(profileId: Long): Completable {
|
fun deleteProfile(profileId: Long): Completable {
|
||||||
return Completable.fromCallable { database.deleteProfile(profileId) }
|
return Completable.fromCallable { database.deleteProfile(profileId) }
|
||||||
|
.doOnComplete { scheduleNotificationProfileSync(profileId) }
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,7 +141,10 @@ class NotificationProfilesRepository {
|
|||||||
SignalStore.notificationProfile.manuallyDisabledAt = now
|
SignalStore.notificationProfile.manuallyDisabledAt = now
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.doOnComplete { AppDependencies.databaseObserver.notifyNotificationProfileObservers() }
|
.doOnComplete {
|
||||||
|
scheduleManualOverrideSync()
|
||||||
|
AppDependencies.databaseObserver.notifyNotificationProfileObservers()
|
||||||
|
}
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,7 +154,10 @@ class NotificationProfilesRepository {
|
|||||||
SignalStore.notificationProfile.manuallyEnabledUntil = enableUntil
|
SignalStore.notificationProfile.manuallyEnabledUntil = enableUntil
|
||||||
SignalStore.notificationProfile.manuallyDisabledAt = now
|
SignalStore.notificationProfile.manuallyDisabledAt = now
|
||||||
}
|
}
|
||||||
.doOnComplete { AppDependencies.databaseObserver.notifyNotificationProfileObservers() }
|
.doOnComplete {
|
||||||
|
scheduleManualOverrideSync()
|
||||||
|
AppDependencies.databaseObserver.notifyNotificationProfileObservers()
|
||||||
|
}
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,9 +168,28 @@ class NotificationProfilesRepository {
|
|||||||
SignalStore.notificationProfile.manuallyEnabledUntil = if (inScheduledWindow) schedule.endDateTime(now.toLocalDateTime()).toMillis() else Long.MAX_VALUE
|
SignalStore.notificationProfile.manuallyEnabledUntil = if (inScheduledWindow) schedule.endDateTime(now.toLocalDateTime()).toMillis() else Long.MAX_VALUE
|
||||||
SignalStore.notificationProfile.manuallyDisabledAt = if (inScheduledWindow) now else 0
|
SignalStore.notificationProfile.manuallyDisabledAt = if (inScheduledWindow) now else 0
|
||||||
}
|
}
|
||||||
.doOnComplete { AppDependencies.databaseObserver.notifyNotificationProfileObservers() }
|
.doOnComplete {
|
||||||
|
scheduleManualOverrideSync()
|
||||||
|
AppDependencies.databaseObserver.notifyNotificationProfileObservers()
|
||||||
|
}
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schedules a sync for a notification profile when it changes
|
||||||
|
*/
|
||||||
|
fun scheduleNotificationProfileSync(profileId: Long) {
|
||||||
|
SignalDatabase.notificationProfiles.markNeedsSync(profileId)
|
||||||
|
StorageSyncHelper.scheduleSyncForDataChange()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schedules a sync for the self when the manual notification profile changes
|
||||||
|
*/
|
||||||
|
private fun scheduleManualOverrideSync() {
|
||||||
|
SignalDatabase.recipients.markNeedsSync(Recipient.self().id)
|
||||||
|
StorageSyncHelper.scheduleSyncForDataChange()
|
||||||
|
}
|
||||||
|
|
||||||
class NotificationProfileNotFoundException : Throwable()
|
class NotificationProfileNotFoundException : Throwable()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,18 +24,12 @@ import org.signal.core.util.requireString
|
|||||||
import org.signal.core.util.select
|
import org.signal.core.util.select
|
||||||
import org.signal.core.util.update
|
import org.signal.core.util.update
|
||||||
import org.signal.core.util.withinTransaction
|
import org.signal.core.util.withinTransaction
|
||||||
import org.signal.libsignal.zkgroup.InvalidInputException
|
|
||||||
import org.signal.libsignal.zkgroup.groups.GroupMasterKey
|
|
||||||
import org.thoughtcrime.securesms.components.settings.app.chats.folders.ChatFolderId
|
import org.thoughtcrime.securesms.components.settings.app.chats.folders.ChatFolderId
|
||||||
import org.thoughtcrime.securesms.components.settings.app.chats.folders.ChatFolderRecord
|
import org.thoughtcrime.securesms.components.settings.app.chats.folders.ChatFolderRecord
|
||||||
import org.thoughtcrime.securesms.database.ThreadTable.Companion.ID
|
import org.thoughtcrime.securesms.database.ThreadTable.Companion.ID
|
||||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||||
import org.thoughtcrime.securesms.groups.BadGroupIdException
|
|
||||||
import org.thoughtcrime.securesms.groups.GroupId
|
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient
|
|
||||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper
|
import org.thoughtcrime.securesms.storage.StorageSyncHelper
|
||||||
import org.whispersystems.signalservice.api.push.ServiceId
|
import org.thoughtcrime.securesms.storage.StorageSyncModels
|
||||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress
|
|
||||||
import org.whispersystems.signalservice.api.storage.SignalChatFolderRecord
|
import org.whispersystems.signalservice.api.storage.SignalChatFolderRecord
|
||||||
import org.whispersystems.signalservice.api.storage.StorageId
|
import org.whispersystems.signalservice.api.storage.StorageId
|
||||||
import org.whispersystems.signalservice.api.util.UuidUtil
|
import org.whispersystems.signalservice.api.util.UuidUtil
|
||||||
@@ -684,10 +678,10 @@ class ChatFolderTables(context: Context?, databaseHelper: SignalDatabase?) : Dat
|
|||||||
RemoteChatFolderRecord.FolderType.UNKNOWN -> throw AssertionError("Folder type cannot be unknown")
|
RemoteChatFolderRecord.FolderType.UNKNOWN -> throw AssertionError("Folder type cannot be unknown")
|
||||||
},
|
},
|
||||||
includedChats = record.proto.includedRecipients
|
includedChats = record.proto.includedRecipients
|
||||||
.mapNotNull { remoteRecipient -> getRecipientIdFromRemoteRecipient(remoteRecipient) }
|
.mapNotNull { remoteRecipient -> StorageSyncModels.remoteToLocalRecipient(remoteRecipient) }
|
||||||
.map { recipient -> SignalDatabase.threads.getOrCreateThreadIdFor(recipient) },
|
.map { recipient -> SignalDatabase.threads.getOrCreateThreadIdFor(recipient) },
|
||||||
excludedChats = record.proto.excludedRecipients
|
excludedChats = record.proto.excludedRecipients
|
||||||
.mapNotNull { remoteRecipient -> getRecipientIdFromRemoteRecipient(remoteRecipient) }
|
.mapNotNull { remoteRecipient -> StorageSyncModels.remoteToLocalRecipient(remoteRecipient) }
|
||||||
.map { recipient -> SignalDatabase.threads.getOrCreateThreadIdFor(recipient) },
|
.map { recipient -> SignalDatabase.threads.getOrCreateThreadIdFor(recipient) },
|
||||||
chatFolderId = chatFolderId,
|
chatFolderId = chatFolderId,
|
||||||
storageServiceId = StorageId.forChatFolder(record.id.raw),
|
storageServiceId = StorageId.forChatFolder(record.id.raw),
|
||||||
@@ -696,34 +690,6 @@ class ChatFolderTables(context: Context?, databaseHelper: SignalDatabase?) : Dat
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses a remote recipient into a local one. Used when configuring the chats of a remote chat folder into a local one.
|
|
||||||
*/
|
|
||||||
private fun getRecipientIdFromRemoteRecipient(remoteRecipient: RemoteChatFolderRecord.Recipient): Recipient? {
|
|
||||||
return if (remoteRecipient.contact != null) {
|
|
||||||
val serviceId = ServiceId.parseOrNull(remoteRecipient.contact!!.serviceId)
|
|
||||||
val e164 = remoteRecipient.contact!!.e164
|
|
||||||
Recipient.externalPush(SignalServiceAddress(serviceId, e164))
|
|
||||||
} else if (remoteRecipient.legacyGroupId != null) {
|
|
||||||
try {
|
|
||||||
Recipient.externalGroupExact(GroupId.v1(remoteRecipient.legacyGroupId!!.toByteArray()))
|
|
||||||
} catch (e: BadGroupIdException) {
|
|
||||||
Log.w(TAG, "Failed to parse groupV1 ID!", e)
|
|
||||||
null
|
|
||||||
}
|
|
||||||
} else if (remoteRecipient.groupMasterKey != null) {
|
|
||||||
try {
|
|
||||||
Recipient.externalGroupExact(GroupId.v2(GroupMasterKey(remoteRecipient.groupMasterKey!!.toByteArray())))
|
|
||||||
} catch (e: InvalidInputException) {
|
|
||||||
Log.w(TAG, "Failed to parse groupV2 master key!", e)
|
|
||||||
null
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Log.w(TAG, "Could not find recipient")
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Collection<Long>.toContentValues(chatFolderId: Long, membershipType: MembershipType): List<ContentValues> {
|
private fun Collection<Long>.toContentValues(chatFolderId: Long, membershipType: MembershipType): List<ContentValues> {
|
||||||
return map {
|
return map {
|
||||||
contentValuesOf(
|
contentValuesOf(
|
||||||
|
|||||||
@@ -5,23 +5,40 @@ package org.thoughtcrime.securesms.database
|
|||||||
import android.content.ContentValues
|
import android.content.ContentValues
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
import android.database.sqlite.SQLiteConstraintException
|
import androidx.core.content.contentValuesOf
|
||||||
|
import org.signal.core.util.Base64
|
||||||
import org.signal.core.util.SqlUtil
|
import org.signal.core.util.SqlUtil
|
||||||
|
import org.signal.core.util.exists
|
||||||
|
import org.signal.core.util.hasUnknownFields
|
||||||
|
import org.signal.core.util.insertInto
|
||||||
import org.signal.core.util.logging.Log
|
import org.signal.core.util.logging.Log
|
||||||
|
import org.signal.core.util.readToList
|
||||||
|
import org.signal.core.util.readToMap
|
||||||
|
import org.signal.core.util.readToSingleLong
|
||||||
|
import org.signal.core.util.readToSingleObject
|
||||||
import org.signal.core.util.requireBoolean
|
import org.signal.core.util.requireBoolean
|
||||||
import org.signal.core.util.requireInt
|
import org.signal.core.util.requireInt
|
||||||
import org.signal.core.util.requireLong
|
import org.signal.core.util.requireLong
|
||||||
import org.signal.core.util.requireNonNullString
|
import org.signal.core.util.requireNonNullString
|
||||||
import org.signal.core.util.requireString
|
import org.signal.core.util.requireString
|
||||||
|
import org.signal.core.util.select
|
||||||
import org.signal.core.util.toInt
|
import org.signal.core.util.toInt
|
||||||
import org.signal.core.util.update
|
import org.signal.core.util.update
|
||||||
|
import org.signal.core.util.withinTransaction
|
||||||
import org.thoughtcrime.securesms.conversation.colors.AvatarColor
|
import org.thoughtcrime.securesms.conversation.colors.AvatarColor
|
||||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||||
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile
|
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile
|
||||||
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfileId
|
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfileId
|
||||||
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfileSchedule
|
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfileSchedule
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||||
|
import org.thoughtcrime.securesms.storage.StorageSyncHelper
|
||||||
|
import org.thoughtcrime.securesms.storage.StorageSyncModels
|
||||||
|
import org.thoughtcrime.securesms.storage.StorageSyncModels.toLocal
|
||||||
|
import org.whispersystems.signalservice.api.storage.SignalNotificationProfileRecord
|
||||||
|
import org.whispersystems.signalservice.api.storage.StorageId
|
||||||
|
import org.whispersystems.signalservice.api.util.UuidUtil
|
||||||
import java.time.DayOfWeek
|
import java.time.DayOfWeek
|
||||||
|
import kotlin.time.Duration.Companion.days
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Database for maintaining Notification Profiles, Notification Profile Schedules, and Notification Profile allowed memebers.
|
* Database for maintaining Notification Profiles, Notification Profile Schedules, and Notification Profile allowed memebers.
|
||||||
@@ -30,6 +47,7 @@ class NotificationProfileTables(context: Context, databaseHelper: SignalDatabase
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val TAG = Log.tag(NotificationProfileTable::class)
|
private val TAG = Log.tag(NotificationProfileTable::class)
|
||||||
|
private val DELETED_LIFESPAN: Long = 30.days.inWholeMilliseconds
|
||||||
|
|
||||||
@JvmField
|
@JvmField
|
||||||
val CREATE_TABLE: Array<String> = arrayOf(NotificationProfileTable.CREATE_TABLE, NotificationProfileScheduleTable.CREATE_TABLE, NotificationProfileAllowedMembersTable.CREATE_TABLE)
|
val CREATE_TABLE: Array<String> = arrayOf(NotificationProfileTable.CREATE_TABLE, NotificationProfileScheduleTable.CREATE_TABLE, NotificationProfileAllowedMembersTable.CREATE_TABLE)
|
||||||
@@ -49,17 +67,23 @@ class NotificationProfileTables(context: Context, databaseHelper: SignalDatabase
|
|||||||
const val ALLOW_ALL_CALLS = "allow_all_calls"
|
const val ALLOW_ALL_CALLS = "allow_all_calls"
|
||||||
const val ALLOW_ALL_MENTIONS = "allow_all_mentions"
|
const val ALLOW_ALL_MENTIONS = "allow_all_mentions"
|
||||||
const val NOTIFICATION_PROFILE_ID = "notification_profile_id"
|
const val NOTIFICATION_PROFILE_ID = "notification_profile_id"
|
||||||
|
const val DELETED_TIMESTAMP_MS = "deleted_timestamp_ms"
|
||||||
|
const val STORAGE_SERVICE_ID = "storage_service_id"
|
||||||
|
const val STORAGE_SERVICE_PROTO = "storage_service_proto"
|
||||||
|
|
||||||
val CREATE_TABLE = """
|
val CREATE_TABLE = """
|
||||||
CREATE TABLE $TABLE_NAME (
|
CREATE TABLE $TABLE_NAME (
|
||||||
$ID INTEGER PRIMARY KEY AUTOINCREMENT,
|
$ID INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
$NAME TEXT NOT NULL UNIQUE,
|
$NAME TEXT NOT NULL,
|
||||||
$EMOJI TEXT NOT NULL,
|
$EMOJI TEXT NOT NULL,
|
||||||
$COLOR TEXT NOT NULL,
|
$COLOR TEXT NOT NULL,
|
||||||
$CREATED_AT INTEGER NOT NULL,
|
$CREATED_AT INTEGER NOT NULL,
|
||||||
$ALLOW_ALL_CALLS INTEGER NOT NULL DEFAULT 0,
|
$ALLOW_ALL_CALLS INTEGER NOT NULL DEFAULT 0,
|
||||||
$ALLOW_ALL_MENTIONS INTEGER NOT NULL DEFAULT 0,
|
$ALLOW_ALL_MENTIONS INTEGER NOT NULL DEFAULT 0,
|
||||||
$NOTIFICATION_PROFILE_ID TEXT DEFAULT NULL
|
$NOTIFICATION_PROFILE_ID TEXT DEFAULT NULL,
|
||||||
|
$DELETED_TIMESTAMP_MS INTEGER DEFAULT 0,
|
||||||
|
$STORAGE_SERVICE_ID TEXT DEFAULT NULL,
|
||||||
|
$STORAGE_SERVICE_PROTO TEXT DEFAULT NULL
|
||||||
)
|
)
|
||||||
"""
|
"""
|
||||||
}
|
}
|
||||||
@@ -114,7 +138,12 @@ class NotificationProfileTables(context: Context, databaseHelper: SignalDatabase
|
|||||||
|
|
||||||
db.beginTransaction()
|
db.beginTransaction()
|
||||||
try {
|
try {
|
||||||
|
if (isDuplicateName(name)) {
|
||||||
|
return NotificationProfileChangeResult.DuplicateName
|
||||||
|
}
|
||||||
|
|
||||||
val notificationProfileId = NotificationProfileId.generate()
|
val notificationProfileId = NotificationProfileId.generate()
|
||||||
|
val storageServiceId = StorageSyncHelper.generateKey()
|
||||||
val profileValues = ContentValues().apply {
|
val profileValues = ContentValues().apply {
|
||||||
put(NotificationProfileTable.NAME, name)
|
put(NotificationProfileTable.NAME, name)
|
||||||
put(NotificationProfileTable.EMOJI, emoji)
|
put(NotificationProfileTable.EMOJI, emoji)
|
||||||
@@ -122,12 +151,10 @@ class NotificationProfileTables(context: Context, databaseHelper: SignalDatabase
|
|||||||
put(NotificationProfileTable.CREATED_AT, createdAt)
|
put(NotificationProfileTable.CREATED_AT, createdAt)
|
||||||
put(NotificationProfileTable.ALLOW_ALL_CALLS, 1)
|
put(NotificationProfileTable.ALLOW_ALL_CALLS, 1)
|
||||||
put(NotificationProfileTable.NOTIFICATION_PROFILE_ID, notificationProfileId.serialize())
|
put(NotificationProfileTable.NOTIFICATION_PROFILE_ID, notificationProfileId.serialize())
|
||||||
|
put(NotificationProfileTable.STORAGE_SERVICE_ID, Base64.encodeWithPadding(storageServiceId))
|
||||||
}
|
}
|
||||||
|
|
||||||
val profileId = db.insert(NotificationProfileTable.TABLE_NAME, null, profileValues)
|
val profileId = db.insert(NotificationProfileTable.TABLE_NAME, null, profileValues)
|
||||||
if (profileId < 0) {
|
|
||||||
return NotificationProfileChangeResult.DuplicateName
|
|
||||||
}
|
|
||||||
|
|
||||||
val scheduleValues = ContentValues().apply {
|
val scheduleValues = ContentValues().apply {
|
||||||
put(NotificationProfileScheduleTable.NOTIFICATION_PROFILE_ID, profileId)
|
put(NotificationProfileScheduleTable.NOTIFICATION_PROFILE_ID, profileId)
|
||||||
@@ -147,7 +174,8 @@ class NotificationProfileTables(context: Context, databaseHelper: SignalDatabase
|
|||||||
createdAt = createdAt,
|
createdAt = createdAt,
|
||||||
schedule = getProfileSchedule(profileId),
|
schedule = getProfileSchedule(profileId),
|
||||||
allowAllCalls = true,
|
allowAllCalls = true,
|
||||||
notificationProfileId = notificationProfileId
|
notificationProfileId = notificationProfileId,
|
||||||
|
storageServiceId = StorageId.forNotificationProfile(storageServiceId)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
} finally {
|
} finally {
|
||||||
@@ -157,6 +185,10 @@ class NotificationProfileTables(context: Context, databaseHelper: SignalDatabase
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun updateProfile(profileId: Long, name: String, emoji: String): NotificationProfileChangeResult {
|
fun updateProfile(profileId: Long, name: String, emoji: String): NotificationProfileChangeResult {
|
||||||
|
if (isDuplicateName(name, profileId)) {
|
||||||
|
return NotificationProfileChangeResult.DuplicateName
|
||||||
|
}
|
||||||
|
|
||||||
val profileValues = ContentValues().apply {
|
val profileValues = ContentValues().apply {
|
||||||
put(NotificationProfileTable.NAME, name)
|
put(NotificationProfileTable.NAME, name)
|
||||||
put(NotificationProfileTable.EMOJI, emoji)
|
put(NotificationProfileTable.EMOJI, emoji)
|
||||||
@@ -164,37 +196,38 @@ class NotificationProfileTables(context: Context, databaseHelper: SignalDatabase
|
|||||||
|
|
||||||
val updateQuery = SqlUtil.buildTrueUpdateQuery(ID_WHERE, SqlUtil.buildArgs(profileId), profileValues)
|
val updateQuery = SqlUtil.buildTrueUpdateQuery(ID_WHERE, SqlUtil.buildArgs(profileId), profileValues)
|
||||||
|
|
||||||
return try {
|
val count = writableDatabase.update(NotificationProfileTable.TABLE_NAME, profileValues, updateQuery.where, updateQuery.whereArgs)
|
||||||
val count = writableDatabase.update(NotificationProfileTable.TABLE_NAME, profileValues, updateQuery.where, updateQuery.whereArgs)
|
if (count > 0) {
|
||||||
if (count > 0) {
|
AppDependencies.databaseObserver.notifyNotificationProfileObservers()
|
||||||
AppDependencies.databaseObserver.notifyNotificationProfileObservers()
|
|
||||||
}
|
|
||||||
|
|
||||||
NotificationProfileChangeResult.Success(getProfile(profileId)!!)
|
|
||||||
} catch (e: SQLiteConstraintException) {
|
|
||||||
NotificationProfileChangeResult.DuplicateName
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return NotificationProfileChangeResult.Success(getProfile(profileId)!!)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateProfile(profile: NotificationProfile): NotificationProfileChangeResult {
|
fun updateProfile(profile: NotificationProfile): NotificationProfileChangeResult {
|
||||||
|
if (isDuplicateName(profile.name, profile.id)) {
|
||||||
|
return NotificationProfileChangeResult.DuplicateName
|
||||||
|
}
|
||||||
|
|
||||||
val db = writableDatabase
|
val db = writableDatabase
|
||||||
|
|
||||||
db.beginTransaction()
|
db.beginTransaction()
|
||||||
try {
|
try {
|
||||||
|
val storageServiceId = profile.storageServiceId?.raw ?: StorageSyncHelper.generateKey()
|
||||||
|
val storageServiceProto = if (profile.storageServiceProto != null) Base64.encodeWithPadding(profile.storageServiceProto) else null
|
||||||
|
|
||||||
val profileValues = ContentValues().apply {
|
val profileValues = ContentValues().apply {
|
||||||
put(NotificationProfileTable.NAME, profile.name)
|
put(NotificationProfileTable.NAME, profile.name)
|
||||||
put(NotificationProfileTable.EMOJI, profile.emoji)
|
put(NotificationProfileTable.EMOJI, profile.emoji)
|
||||||
put(NotificationProfileTable.ALLOW_ALL_CALLS, profile.allowAllCalls.toInt())
|
put(NotificationProfileTable.ALLOW_ALL_CALLS, profile.allowAllCalls.toInt())
|
||||||
put(NotificationProfileTable.ALLOW_ALL_MENTIONS, profile.allowAllMentions.toInt())
|
put(NotificationProfileTable.ALLOW_ALL_MENTIONS, profile.allowAllMentions.toInt())
|
||||||
|
put(NotificationProfileTable.STORAGE_SERVICE_ID, Base64.encodeWithPadding(storageServiceId))
|
||||||
|
put(NotificationProfileTable.STORAGE_SERVICE_PROTO, storageServiceProto)
|
||||||
}
|
}
|
||||||
|
|
||||||
val updateQuery = SqlUtil.buildTrueUpdateQuery(ID_WHERE, SqlUtil.buildArgs(profile.id), profileValues)
|
val updateQuery = SqlUtil.buildTrueUpdateQuery(ID_WHERE, SqlUtil.buildArgs(profile.id), profileValues)
|
||||||
|
|
||||||
try {
|
db.update(NotificationProfileTable.TABLE_NAME, profileValues, updateQuery.where, updateQuery.whereArgs)
|
||||||
db.update(NotificationProfileTable.TABLE_NAME, profileValues, updateQuery.where, updateQuery.whereArgs)
|
|
||||||
} catch (e: SQLiteConstraintException) {
|
|
||||||
return NotificationProfileChangeResult.DuplicateName
|
|
||||||
}
|
|
||||||
|
|
||||||
updateSchedule(profile.schedule, true)
|
updateSchedule(profile.schedule, true)
|
||||||
|
|
||||||
@@ -280,16 +313,16 @@ class NotificationProfileTables(context: Context, databaseHelper: SignalDatabase
|
|||||||
return getProfile(profileId)!!
|
return getProfile(profileId)!!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all undeleted notification profiles
|
||||||
|
*/
|
||||||
fun getProfiles(): List<NotificationProfile> {
|
fun getProfiles(): List<NotificationProfile> {
|
||||||
val profiles: MutableList<NotificationProfile> = mutableListOf()
|
return readableDatabase
|
||||||
|
.select()
|
||||||
readableDatabase.query(NotificationProfileTable.TABLE_NAME, null, null, null, null, null, null).use { cursor ->
|
.from(NotificationProfileTable.TABLE_NAME)
|
||||||
while (cursor.moveToNext()) {
|
.where("${NotificationProfileTable.DELETED_TIMESTAMP_MS} = 0")
|
||||||
profiles += getProfile(cursor)
|
.run()
|
||||||
}
|
.readToList { cursor -> getProfile(cursor) }
|
||||||
}
|
|
||||||
|
|
||||||
return profiles
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getProfile(profileId: Long): NotificationProfile? {
|
fun getProfile(profileId: Long): NotificationProfile? {
|
||||||
@@ -302,11 +335,196 @@ class NotificationProfileTables(context: Context, databaseHelper: SignalDatabase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getProfile(query: SqlUtil.Query): NotificationProfile? {
|
||||||
|
return readableDatabase
|
||||||
|
.select()
|
||||||
|
.from(NotificationProfileTable.TABLE_NAME)
|
||||||
|
.where(query.where, query.whereArgs)
|
||||||
|
.run()
|
||||||
|
.readToSingleObject { cursor -> getProfile(cursor) }
|
||||||
|
}
|
||||||
|
|
||||||
fun deleteProfile(profileId: Long) {
|
fun deleteProfile(profileId: Long) {
|
||||||
writableDatabase.delete(NotificationProfileTable.TABLE_NAME, ID_WHERE, SqlUtil.buildArgs(profileId))
|
writableDatabase.withinTransaction { db ->
|
||||||
|
db.update(NotificationProfileTable.TABLE_NAME)
|
||||||
|
.values(NotificationProfileTable.DELETED_TIMESTAMP_MS to System.currentTimeMillis())
|
||||||
|
.where("${NotificationProfileTable.ID} = ?", profileId)
|
||||||
|
.run()
|
||||||
|
}
|
||||||
|
|
||||||
AppDependencies.databaseObserver.notifyNotificationProfileObservers()
|
AppDependencies.databaseObserver.notifyNotificationProfileObservers()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun markNeedsSync(profileId: Long) {
|
||||||
|
writableDatabase.withinTransaction {
|
||||||
|
rotateStorageId(profileId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun applyStorageIdUpdate(id: NotificationProfileId, storageId: StorageId) {
|
||||||
|
applyStorageIdUpdates(hashMapOf(id to storageId))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun applyStorageIdUpdates(storageIds: Map<NotificationProfileId, StorageId>) {
|
||||||
|
writableDatabase.withinTransaction { db ->
|
||||||
|
storageIds.forEach { (notificationProfileId, storageId) ->
|
||||||
|
db.update(NotificationProfileTable.TABLE_NAME)
|
||||||
|
.values(NotificationProfileTable.STORAGE_SERVICE_ID to Base64.encodeWithPadding(storageId.raw))
|
||||||
|
.where("${NotificationProfileTable.NOTIFICATION_PROFILE_ID} = ?", notificationProfileId.serialize())
|
||||||
|
.run()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun insertNotificationProfileFromStorageSync(notificationProfileRecord: SignalNotificationProfileRecord) {
|
||||||
|
val profile = notificationProfileRecord.proto
|
||||||
|
writableDatabase.withinTransaction { db ->
|
||||||
|
val storageServiceProto = if (notificationProfileRecord.proto.hasUnknownFields()) Base64.encodeWithPadding(notificationProfileRecord.serializedUnknowns!!) else null
|
||||||
|
|
||||||
|
val id = db.insertInto(NotificationProfileTable.TABLE_NAME)
|
||||||
|
.values(
|
||||||
|
contentValuesOf(
|
||||||
|
NotificationProfileTable.NAME to profile.name,
|
||||||
|
NotificationProfileTable.EMOJI to profile.emoji.orEmpty(),
|
||||||
|
NotificationProfileTable.COLOR to (AvatarColor.fromColor(profile.color)?.serialize() ?: NotificationProfile.DEFAULT_NOTIFICATION_PROFILE_COLOR.serialize()),
|
||||||
|
NotificationProfileTable.CREATED_AT to profile.createdAtMs,
|
||||||
|
NotificationProfileTable.ALLOW_ALL_CALLS to profile.allowAllCalls,
|
||||||
|
NotificationProfileTable.ALLOW_ALL_MENTIONS to profile.allowAllMentions,
|
||||||
|
NotificationProfileTable.NOTIFICATION_PROFILE_ID to NotificationProfileId(UuidUtil.parseOrThrow(profile.id)).serialize(),
|
||||||
|
NotificationProfileTable.DELETED_TIMESTAMP_MS to profile.deletedAtTimestampMs,
|
||||||
|
NotificationProfileTable.STORAGE_SERVICE_ID to Base64.encodeWithPadding(notificationProfileRecord.id.raw),
|
||||||
|
NotificationProfileTable.STORAGE_SERVICE_PROTO to storageServiceProto
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
|
||||||
|
db.insertInto(NotificationProfileScheduleTable.TABLE_NAME)
|
||||||
|
.values(
|
||||||
|
contentValuesOf(
|
||||||
|
NotificationProfileScheduleTable.NOTIFICATION_PROFILE_ID to id,
|
||||||
|
NotificationProfileScheduleTable.ENABLED to profile.scheduleEnabled.toInt(),
|
||||||
|
NotificationProfileScheduleTable.START to profile.scheduleStartTime,
|
||||||
|
NotificationProfileScheduleTable.END to profile.scheduleEndTime,
|
||||||
|
NotificationProfileScheduleTable.DAYS_ENABLED to profile.scheduleDaysEnabled.map { it.toLocal() }.toSet().serialize()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
|
||||||
|
profile.allowedMembers
|
||||||
|
.mapNotNull { remoteRecipient -> StorageSyncModels.remoteToLocalRecipient(remoteRecipient) }
|
||||||
|
.forEach {
|
||||||
|
db.insertInto(NotificationProfileAllowedMembersTable.TABLE_NAME)
|
||||||
|
.values(
|
||||||
|
NotificationProfileAllowedMembersTable.NOTIFICATION_PROFILE_ID to id,
|
||||||
|
NotificationProfileAllowedMembersTable.RECIPIENT_ID to it.id.serialize()
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AppDependencies.databaseObserver.notifyNotificationProfileObservers()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateNotificationProfileFromStorageSync(notificationProfileRecord: SignalNotificationProfileRecord) {
|
||||||
|
val profile = notificationProfileRecord.proto
|
||||||
|
val notificationProfileId = NotificationProfileId(UuidUtil.parseOrThrow(profile.id))
|
||||||
|
|
||||||
|
val profileId = readableDatabase
|
||||||
|
.select(NotificationProfileTable.ID)
|
||||||
|
.from(NotificationProfileTable.TABLE_NAME)
|
||||||
|
.where("${NotificationProfileTable.NOTIFICATION_PROFILE_ID} = ?", notificationProfileId.serialize())
|
||||||
|
.run()
|
||||||
|
.readToSingleLong()
|
||||||
|
|
||||||
|
val scheduleId = readableDatabase
|
||||||
|
.select(NotificationProfileScheduleTable.ID)
|
||||||
|
.from(NotificationProfileScheduleTable.TABLE_NAME)
|
||||||
|
.where("${NotificationProfileScheduleTable.NOTIFICATION_PROFILE_ID} = ?", profileId)
|
||||||
|
.run()
|
||||||
|
.readToSingleLong()
|
||||||
|
|
||||||
|
updateProfile(
|
||||||
|
NotificationProfile(
|
||||||
|
id = profileId,
|
||||||
|
name = profile.name,
|
||||||
|
emoji = profile.emoji.orEmpty(),
|
||||||
|
color = AvatarColor.fromColor(profile.color) ?: NotificationProfile.DEFAULT_NOTIFICATION_PROFILE_COLOR,
|
||||||
|
createdAt = profile.createdAtMs,
|
||||||
|
allowAllCalls = profile.allowAllCalls,
|
||||||
|
allowAllMentions = profile.allowAllMentions,
|
||||||
|
schedule = NotificationProfileSchedule(
|
||||||
|
scheduleId,
|
||||||
|
profile.scheduleEnabled,
|
||||||
|
profile.scheduleStartTime,
|
||||||
|
profile.scheduleEndTime,
|
||||||
|
profile.scheduleDaysEnabled.map { it.toLocal() }.toSet()
|
||||||
|
),
|
||||||
|
allowedMembers = profile.allowedMembers.mapNotNull { remoteRecipient -> StorageSyncModels.remoteToLocalRecipient(remoteRecipient)?.id }.toSet(),
|
||||||
|
notificationProfileId = notificationProfileId,
|
||||||
|
deletedTimestampMs = profile.deletedAtTimestampMs,
|
||||||
|
storageServiceId = StorageId.forNotificationProfile(notificationProfileRecord.id.raw),
|
||||||
|
storageServiceProto = notificationProfileRecord.serializedUnknowns
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getStorageSyncIdsMap(): Map<NotificationProfileId, StorageId> {
|
||||||
|
return readableDatabase
|
||||||
|
.select(NotificationProfileTable.NOTIFICATION_PROFILE_ID, NotificationProfileTable.STORAGE_SERVICE_ID)
|
||||||
|
.from(NotificationProfileTable.TABLE_NAME)
|
||||||
|
.where("${NotificationProfileTable.STORAGE_SERVICE_ID} IS NOT NULL")
|
||||||
|
.run()
|
||||||
|
.readToMap { cursor ->
|
||||||
|
val id = NotificationProfileId.from(cursor.requireNonNullString(NotificationProfileTable.NOTIFICATION_PROFILE_ID))
|
||||||
|
val encodedKey = cursor.requireNonNullString(NotificationProfileTable.STORAGE_SERVICE_ID)
|
||||||
|
val key = Base64.decodeOrThrow(encodedKey)
|
||||||
|
id to StorageId.forNotificationProfile(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getStorageSyncIds(): List<StorageId> {
|
||||||
|
return readableDatabase
|
||||||
|
.select(NotificationProfileTable.STORAGE_SERVICE_ID)
|
||||||
|
.from(NotificationProfileTable.TABLE_NAME)
|
||||||
|
.where("${NotificationProfileTable.STORAGE_SERVICE_ID} IS NOT NULL")
|
||||||
|
.run()
|
||||||
|
.readToList { cursor ->
|
||||||
|
val encodedKey = cursor.requireNonNullString(NotificationProfileTable.STORAGE_SERVICE_ID)
|
||||||
|
val key = Base64.decodeOrThrow(encodedKey)
|
||||||
|
StorageId.forNotificationProfile(key)
|
||||||
|
}.also { Log.i(TAG, "${it.size} profiles have storage ids.") }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes storageIds from notification profiles that have been deleted for [DELETED_LIFESPAN].
|
||||||
|
*/
|
||||||
|
fun removeStorageIdsFromOldDeletedProfiles(now: Long): Int {
|
||||||
|
return writableDatabase
|
||||||
|
.update(NotificationProfileTable.TABLE_NAME)
|
||||||
|
.values(NotificationProfileTable.STORAGE_SERVICE_ID to null)
|
||||||
|
.where("${NotificationProfileTable.STORAGE_SERVICE_ID} NOT NULL AND ${NotificationProfileTable.DELETED_TIMESTAMP_MS} > 0 AND ${NotificationProfileTable.DELETED_TIMESTAMP_MS} < ?", now - DELETED_LIFESPAN)
|
||||||
|
.run()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes storageIds of profiles that are local only and deleted
|
||||||
|
*/
|
||||||
|
fun removeStorageIdsFromLocalOnlyDeletedProfiles(storageIds: Collection<StorageId>): Int {
|
||||||
|
var updated = 0
|
||||||
|
|
||||||
|
SqlUtil.buildCollectionQuery(NotificationProfileTable.STORAGE_SERVICE_ID, storageIds.map { Base64.encodeWithPadding(it.raw) }, "${NotificationProfileTable.DELETED_TIMESTAMP_MS} > 0 AND")
|
||||||
|
.forEach {
|
||||||
|
updated += writableDatabase.update(
|
||||||
|
NotificationProfileTable.TABLE_NAME,
|
||||||
|
contentValuesOf(NotificationProfileTable.STORAGE_SERVICE_ID to null),
|
||||||
|
it.where,
|
||||||
|
it.whereArgs
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return updated
|
||||||
|
}
|
||||||
|
|
||||||
override fun remapRecipient(fromId: RecipientId, toId: RecipientId) {
|
override fun remapRecipient(fromId: RecipientId, toId: RecipientId) {
|
||||||
val count = writableDatabase
|
val count = writableDatabase
|
||||||
.update(NotificationProfileAllowedMembersTable.TABLE_NAME)
|
.update(NotificationProfileAllowedMembersTable.TABLE_NAME)
|
||||||
@@ -331,7 +549,10 @@ class NotificationProfileTables(context: Context, databaseHelper: SignalDatabase
|
|||||||
allowAllMentions = cursor.requireBoolean(NotificationProfileTable.ALLOW_ALL_MENTIONS),
|
allowAllMentions = cursor.requireBoolean(NotificationProfileTable.ALLOW_ALL_MENTIONS),
|
||||||
schedule = getProfileSchedule(profileId),
|
schedule = getProfileSchedule(profileId),
|
||||||
allowedMembers = getProfileAllowedMembers(profileId),
|
allowedMembers = getProfileAllowedMembers(profileId),
|
||||||
notificationProfileId = NotificationProfileId.from(cursor.requireNonNullString(NotificationProfileTable.NOTIFICATION_PROFILE_ID))
|
notificationProfileId = NotificationProfileId.from(cursor.requireNonNullString(NotificationProfileTable.NOTIFICATION_PROFILE_ID)),
|
||||||
|
deletedTimestampMs = cursor.requireLong(NotificationProfileTable.DELETED_TIMESTAMP_MS),
|
||||||
|
storageServiceId = cursor.requireString(NotificationProfileTable.STORAGE_SERVICE_ID)?.let { StorageId.forNotificationProfile(Base64.decodeNullableOrThrow(it)) },
|
||||||
|
storageServiceProto = Base64.decodeOrNull(cursor.requireString(NotificationProfileTable.STORAGE_SERVICE_PROTO))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -372,6 +593,24 @@ class NotificationProfileTables(context: Context, databaseHelper: SignalDatabase
|
|||||||
return allowed
|
return allowed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun rotateStorageId(id: Long) {
|
||||||
|
writableDatabase
|
||||||
|
.update(NotificationProfileTable.TABLE_NAME)
|
||||||
|
.values(NotificationProfileTable.STORAGE_SERVICE_ID to Base64.encodeWithPadding(StorageSyncHelper.generateKey()))
|
||||||
|
.where("${NotificationProfileTable.ID} = ?", id)
|
||||||
|
.run()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks that there is no other notification profile with the same [name]
|
||||||
|
*/
|
||||||
|
private fun isDuplicateName(name: String, id: Long = -1): Boolean {
|
||||||
|
return readableDatabase
|
||||||
|
.exists(NotificationProfileTable.TABLE_NAME)
|
||||||
|
.where("${NotificationProfileTable.NAME} = ? AND ${NotificationProfileTable.DELETED_TIMESTAMP_MS} = 0 AND ${NotificationProfileTable.ID} != ?", name, id)
|
||||||
|
.run()
|
||||||
|
}
|
||||||
|
|
||||||
sealed class NotificationProfileChangeResult {
|
sealed class NotificationProfileChangeResult {
|
||||||
data class Success(val notificationProfile: NotificationProfile) : NotificationProfileChangeResult()
|
data class Success(val notificationProfile: NotificationProfile) : NotificationProfileChangeResult()
|
||||||
object DuplicateName : NotificationProfileChangeResult()
|
object DuplicateName : NotificationProfileChangeResult()
|
||||||
|
|||||||
@@ -131,6 +131,7 @@ import org.thoughtcrime.securesms.database.helpers.migration.V273_FixUnreadOrigi
|
|||||||
import org.thoughtcrime.securesms.database.helpers.migration.V274_BackupMediaSnapshotLastSeenOnRemote
|
import org.thoughtcrime.securesms.database.helpers.migration.V274_BackupMediaSnapshotLastSeenOnRemote
|
||||||
import org.thoughtcrime.securesms.database.helpers.migration.V275_EnsureDefaultAllChatsFolder
|
import org.thoughtcrime.securesms.database.helpers.migration.V275_EnsureDefaultAllChatsFolder
|
||||||
import org.thoughtcrime.securesms.database.helpers.migration.V276_AttachmentCdnDefaultValueMigration
|
import org.thoughtcrime.securesms.database.helpers.migration.V276_AttachmentCdnDefaultValueMigration
|
||||||
|
import org.thoughtcrime.securesms.database.helpers.migration.V277_AddNotificationProfileStorageSync
|
||||||
import org.thoughtcrime.securesms.database.SQLiteDatabase as SignalSqliteDatabase
|
import org.thoughtcrime.securesms.database.SQLiteDatabase as SignalSqliteDatabase
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -267,10 +268,11 @@ object SignalDatabaseMigrations {
|
|||||||
273 to V273_FixUnreadOriginalMessages,
|
273 to V273_FixUnreadOriginalMessages,
|
||||||
274 to V274_BackupMediaSnapshotLastSeenOnRemote,
|
274 to V274_BackupMediaSnapshotLastSeenOnRemote,
|
||||||
275 to V275_EnsureDefaultAllChatsFolder,
|
275 to V275_EnsureDefaultAllChatsFolder,
|
||||||
276 to V276_AttachmentCdnDefaultValueMigration
|
276 to V276_AttachmentCdnDefaultValueMigration,
|
||||||
|
277 to V277_AddNotificationProfileStorageSync
|
||||||
)
|
)
|
||||||
|
|
||||||
const val DATABASE_VERSION = 276
|
const val DATABASE_VERSION = 277
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun migrate(context: Application, db: SignalSqliteDatabase, oldVersion: Int, newVersion: Int) {
|
fun migrate(context: Application, db: SignalSqliteDatabase, oldVersion: Int, newVersion: Int) {
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.thoughtcrime.securesms.database.helpers.migration
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import org.signal.core.util.Base64
|
||||||
|
import org.signal.core.util.readToList
|
||||||
|
import org.signal.core.util.requireLong
|
||||||
|
import org.thoughtcrime.securesms.database.SQLiteDatabase
|
||||||
|
import org.thoughtcrime.securesms.storage.StorageSyncHelper
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds columns to notification profiles to support storage service, drops names unique constraint, sets all profiles with a storage service id.
|
||||||
|
*/
|
||||||
|
object V277_AddNotificationProfileStorageSync : SignalDatabaseMigration {
|
||||||
|
override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||||
|
// Rebuild table to drop unique constraint on 'name'
|
||||||
|
db.execSQL(
|
||||||
|
"""
|
||||||
|
CREATE TABLE notification_profile_temp (
|
||||||
|
_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
emoji TEXT NOT NULL,
|
||||||
|
color TEXT NOT NULL,
|
||||||
|
created_at INTEGER NOT NULL,
|
||||||
|
allow_all_calls INTEGER NOT NULL DEFAULT 0,
|
||||||
|
allow_all_mentions INTEGER NOT NULL DEFAULT 0,
|
||||||
|
notification_profile_id TEXT DEFAULT NULL,
|
||||||
|
deleted_timestamp_ms INTEGER DEFAULT 0,
|
||||||
|
storage_service_id TEXT DEFAULT NULL,
|
||||||
|
storage_service_proto TEXT DEFAULT NULL
|
||||||
|
)
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
|
||||||
|
db.execSQL("INSERT INTO notification_profile_temp (_id, name, emoji, color, created_at, allow_all_calls, allow_all_mentions, notification_profile_id) SELECT _id, name, emoji, color, created_at, allow_all_calls, allow_all_mentions, notification_profile_id FROM notification_profile")
|
||||||
|
db.execSQL("DROP TABLE notification_profile")
|
||||||
|
db.execSQL("ALTER TABLE notification_profile_temp RENAME TO notification_profile")
|
||||||
|
|
||||||
|
// Initialize all profiles with a storage service id
|
||||||
|
db.rawQuery("SELECT _id FROM notification_profile")
|
||||||
|
.readToList { it.requireLong("_id") }
|
||||||
|
.forEach { id ->
|
||||||
|
db.execSQL("UPDATE notification_profile SET storage_service_id = '${Base64.encodeWithPadding(StorageSyncHelper.generateKey())}' WHERE _id = $id")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,12 +5,14 @@ import org.signal.core.util.logging.Log
|
|||||||
import org.signal.core.util.logging.logI
|
import org.signal.core.util.logging.logI
|
||||||
import org.thoughtcrime.securesms.components.settings.app.chats.folders.ChatFolderId
|
import org.thoughtcrime.securesms.components.settings.app.chats.folders.ChatFolderId
|
||||||
import org.thoughtcrime.securesms.database.ChatFolderTables.ChatFolderTable
|
import org.thoughtcrime.securesms.database.ChatFolderTables.ChatFolderTable
|
||||||
|
import org.thoughtcrime.securesms.database.NotificationProfileTables
|
||||||
import org.thoughtcrime.securesms.database.RecipientTable
|
import org.thoughtcrime.securesms.database.RecipientTable
|
||||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||||
import org.thoughtcrime.securesms.jobmanager.Job
|
import org.thoughtcrime.securesms.jobmanager.Job
|
||||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
|
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
|
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfileId
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper
|
import org.thoughtcrime.securesms.storage.StorageSyncHelper
|
||||||
@@ -110,6 +112,18 @@ class StorageForcePushJob private constructor(parameters: Parameters) : BaseJob(
|
|||||||
inserts.addAll(newChatFolderInserts)
|
inserts.addAll(newChatFolderInserts)
|
||||||
allNewStorageIds.addAll(newChatFolderStorageIds.values)
|
allNewStorageIds.addAll(newChatFolderStorageIds.values)
|
||||||
|
|
||||||
|
val oldNotificationProfileStorageIds = SignalDatabase.notificationProfiles.getStorageSyncIdsMap()
|
||||||
|
val newNotificationProfileStorageIds = generateNotificationProfileStorageIds(oldNotificationProfileStorageIds)
|
||||||
|
val newNotificationProfileInserts: List<SignalStorageRecord> = oldNotificationProfileStorageIds.keys
|
||||||
|
.mapNotNull {
|
||||||
|
val query = SqlUtil.buildQuery("${NotificationProfileTables.NotificationProfileTable.NOTIFICATION_PROFILE_ID} = ?", it)
|
||||||
|
SignalDatabase.notificationProfiles.getProfile(query)
|
||||||
|
}
|
||||||
|
.map { record -> StorageSyncModels.localToRemoteRecord(record, newNotificationProfileStorageIds[record.notificationProfileId]!!.raw) }
|
||||||
|
|
||||||
|
inserts.addAll(newNotificationProfileInserts)
|
||||||
|
allNewStorageIds.addAll(newNotificationProfileStorageIds.values)
|
||||||
|
|
||||||
val recordIkm: RecordIkm? = if (Recipient.self().storageServiceEncryptionV2Capability.isSupported) {
|
val recordIkm: RecordIkm? = if (Recipient.self().storageServiceEncryptionV2Capability.isSupported) {
|
||||||
Log.i(TAG, "Generating and including a new recordIkm.")
|
Log.i(TAG, "Generating and including a new recordIkm.")
|
||||||
RecordIkm.generate()
|
RecordIkm.generate()
|
||||||
@@ -151,6 +165,7 @@ class StorageForcePushJob private constructor(parameters: Parameters) : BaseJob(
|
|||||||
SignalDatabase.recipients.applyStorageIdUpdates(newContactStorageIds)
|
SignalDatabase.recipients.applyStorageIdUpdates(newContactStorageIds)
|
||||||
SignalDatabase.recipients.applyStorageIdUpdates(Collections.singletonMap(Recipient.self().id, accountRecord.id))
|
SignalDatabase.recipients.applyStorageIdUpdates(Collections.singletonMap(Recipient.self().id, accountRecord.id))
|
||||||
SignalDatabase.chatFolders.applyStorageIdUpdates(newChatFolderStorageIds)
|
SignalDatabase.chatFolders.applyStorageIdUpdates(newChatFolderStorageIds)
|
||||||
|
SignalDatabase.notificationProfiles.applyStorageIdUpdates(newNotificationProfileStorageIds)
|
||||||
SignalDatabase.unknownStorageIds.deleteAll()
|
SignalDatabase.unknownStorageIds.deleteAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,6 +195,12 @@ class StorageForcePushJob private constructor(parameters: Parameters) : BaseJob(
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun generateNotificationProfileStorageIds(oldKeys: Map<NotificationProfileId, StorageId>): Map<NotificationProfileId, StorageId> {
|
||||||
|
return oldKeys.mapValues { (_, value) ->
|
||||||
|
value.withNewBytes(StorageSyncHelper.generateKey())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class Factory : Job.Factory<StorageForcePushJob?> {
|
class Factory : Job.Factory<StorageForcePushJob?> {
|
||||||
override fun create(parameters: Parameters, serializedData: ByteArray?): StorageForcePushJob {
|
override fun create(parameters: Parameters, serializedData: ByteArray?): StorageForcePushJob {
|
||||||
return StorageForcePushJob(parameters)
|
return StorageForcePushJob(parameters)
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import org.signal.core.util.logging.Log
|
|||||||
import org.signal.core.util.withinTransaction
|
import org.signal.core.util.withinTransaction
|
||||||
import org.signal.libsignal.protocol.InvalidKeyException
|
import org.signal.libsignal.protocol.InvalidKeyException
|
||||||
import org.thoughtcrime.securesms.database.ChatFolderTables.ChatFolderTable
|
import org.thoughtcrime.securesms.database.ChatFolderTables.ChatFolderTable
|
||||||
|
import org.thoughtcrime.securesms.database.NotificationProfileTables
|
||||||
import org.thoughtcrime.securesms.database.RecipientTable
|
import org.thoughtcrime.securesms.database.RecipientTable
|
||||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||||
@@ -24,6 +25,7 @@ import org.thoughtcrime.securesms.storage.ChatFolderRecordProcessor
|
|||||||
import org.thoughtcrime.securesms.storage.ContactRecordProcessor
|
import org.thoughtcrime.securesms.storage.ContactRecordProcessor
|
||||||
import org.thoughtcrime.securesms.storage.GroupV1RecordProcessor
|
import org.thoughtcrime.securesms.storage.GroupV1RecordProcessor
|
||||||
import org.thoughtcrime.securesms.storage.GroupV2RecordProcessor
|
import org.thoughtcrime.securesms.storage.GroupV2RecordProcessor
|
||||||
|
import org.thoughtcrime.securesms.storage.NotificationProfileRecordProcessor
|
||||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper
|
import org.thoughtcrime.securesms.storage.StorageSyncHelper
|
||||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper.WriteOperationResult
|
import org.thoughtcrime.securesms.storage.StorageSyncHelper.WriteOperationResult
|
||||||
import org.thoughtcrime.securesms.storage.StorageSyncModels
|
import org.thoughtcrime.securesms.storage.StorageSyncModels
|
||||||
@@ -40,6 +42,7 @@ import org.whispersystems.signalservice.api.storage.SignalChatFolderRecord
|
|||||||
import org.whispersystems.signalservice.api.storage.SignalContactRecord
|
import org.whispersystems.signalservice.api.storage.SignalContactRecord
|
||||||
import org.whispersystems.signalservice.api.storage.SignalGroupV1Record
|
import org.whispersystems.signalservice.api.storage.SignalGroupV1Record
|
||||||
import org.whispersystems.signalservice.api.storage.SignalGroupV2Record
|
import org.whispersystems.signalservice.api.storage.SignalGroupV2Record
|
||||||
|
import org.whispersystems.signalservice.api.storage.SignalNotificationProfileRecord
|
||||||
import org.whispersystems.signalservice.api.storage.SignalStorageManifest
|
import org.whispersystems.signalservice.api.storage.SignalStorageManifest
|
||||||
import org.whispersystems.signalservice.api.storage.SignalStorageRecord
|
import org.whispersystems.signalservice.api.storage.SignalStorageRecord
|
||||||
import org.whispersystems.signalservice.api.storage.SignalStoryDistributionListRecord
|
import org.whispersystems.signalservice.api.storage.SignalStoryDistributionListRecord
|
||||||
@@ -53,6 +56,7 @@ import org.whispersystems.signalservice.api.storage.toSignalChatFolderRecord
|
|||||||
import org.whispersystems.signalservice.api.storage.toSignalContactRecord
|
import org.whispersystems.signalservice.api.storage.toSignalContactRecord
|
||||||
import org.whispersystems.signalservice.api.storage.toSignalGroupV1Record
|
import org.whispersystems.signalservice.api.storage.toSignalGroupV1Record
|
||||||
import org.whispersystems.signalservice.api.storage.toSignalGroupV2Record
|
import org.whispersystems.signalservice.api.storage.toSignalGroupV2Record
|
||||||
|
import org.whispersystems.signalservice.api.storage.toSignalNotificationProfileRecord
|
||||||
import org.whispersystems.signalservice.api.storage.toSignalStoryDistributionListRecord
|
import org.whispersystems.signalservice.api.storage.toSignalStoryDistributionListRecord
|
||||||
import org.whispersystems.signalservice.internal.push.SyncMessage
|
import org.whispersystems.signalservice.internal.push.SyncMessage
|
||||||
import org.whispersystems.signalservice.internal.storage.protos.ManifestRecord
|
import org.whispersystems.signalservice.internal.storage.protos.ManifestRecord
|
||||||
@@ -279,9 +283,10 @@ class StorageSyncJob private constructor(parameters: Parameters, private var loc
|
|||||||
if (idDifference.localOnlyIds.isNotEmpty()) {
|
if (idDifference.localOnlyIds.isNotEmpty()) {
|
||||||
val updatedRecipients = SignalDatabase.recipients.removeStorageIdsFromLocalOnlyUnregisteredRecipients(idDifference.localOnlyIds)
|
val updatedRecipients = SignalDatabase.recipients.removeStorageIdsFromLocalOnlyUnregisteredRecipients(idDifference.localOnlyIds)
|
||||||
val updatedFolders = SignalDatabase.chatFolders.removeStorageIdsFromLocalOnlyDeletedFolders(idDifference.localOnlyIds)
|
val updatedFolders = SignalDatabase.chatFolders.removeStorageIdsFromLocalOnlyDeletedFolders(idDifference.localOnlyIds)
|
||||||
|
val updatedProfiles = SignalDatabase.notificationProfiles.removeStorageIdsFromLocalOnlyDeletedProfiles(idDifference.localOnlyIds)
|
||||||
|
|
||||||
if (updatedRecipients > 0 || updatedFolders > 0) {
|
if (updatedRecipients > 0 || updatedFolders > 0 || updatedProfiles > 0) {
|
||||||
Log.w(TAG, "Found $updatedRecipients recipients and $updatedFolders folders that were deleted remotely but only marked unregistered/deleted locally. Removed those from local store. Recalculating diff.")
|
Log.w(TAG, "Found $updatedRecipients recipients, $updatedFolders folders, $updatedProfiles notification profiles that were deleted remotely but only marked unregistered/deleted locally. Removed those from local store. Recalculating diff.")
|
||||||
|
|
||||||
localStorageIdsBeforeMerge = getAllLocalStorageIds(self)
|
localStorageIdsBeforeMerge = getAllLocalStorageIds(self)
|
||||||
idDifference = StorageSyncHelper.findIdDifference(remoteManifest.storageIds, localStorageIdsBeforeMerge)
|
idDifference = StorageSyncHelper.findIdDifference(remoteManifest.storageIds, localStorageIdsBeforeMerge)
|
||||||
@@ -310,7 +315,7 @@ class StorageSyncJob private constructor(parameters: Parameters, private var loc
|
|||||||
|
|
||||||
db.beginTransaction()
|
db.beginTransaction()
|
||||||
try {
|
try {
|
||||||
Log.i(TAG, "[Remote Sync] Remote-Only :: Contacts: ${remoteOnly.contacts.size}, GV1: ${remoteOnly.gv1.size}, GV2: ${remoteOnly.gv2.size}, Account: ${remoteOnly.account.size}, DLists: ${remoteOnly.storyDistributionLists.size}, call links: ${remoteOnly.callLinkRecords.size}, chat folders: ${remoteOnly.chatFolderRecords.size}")
|
Log.i(TAG, "[Remote Sync] Remote-Only :: Contacts: ${remoteOnly.contacts.size}, GV1: ${remoteOnly.gv1.size}, GV2: ${remoteOnly.gv2.size}, Account: ${remoteOnly.account.size}, DLists: ${remoteOnly.storyDistributionLists.size}, call links: ${remoteOnly.callLinkRecords.size}, chat folders: ${remoteOnly.chatFolderRecords.size}, notification profiles: ${remoteOnly.notificationProfileRecords.size}")
|
||||||
|
|
||||||
processKnownRecords(context, remoteOnly)
|
processKnownRecords(context, remoteOnly)
|
||||||
|
|
||||||
@@ -352,13 +357,10 @@ class StorageSyncJob private constructor(parameters: Parameters, private var loc
|
|||||||
self = freshSelf()
|
self = freshSelf()
|
||||||
|
|
||||||
val removedUnregistered = SignalDatabase.recipients.removeStorageIdsFromOldUnregisteredRecipients(System.currentTimeMillis())
|
val removedUnregistered = SignalDatabase.recipients.removeStorageIdsFromOldUnregisteredRecipients(System.currentTimeMillis())
|
||||||
if (removedUnregistered > 0) {
|
|
||||||
Log.i(TAG, "Removed $removedUnregistered recipients from storage service that have been unregistered for longer than 30 days.")
|
|
||||||
}
|
|
||||||
|
|
||||||
val removedDeletedFolders = SignalDatabase.chatFolders.removeStorageIdsFromOldDeletedFolders(System.currentTimeMillis())
|
val removedDeletedFolders = SignalDatabase.chatFolders.removeStorageIdsFromOldDeletedFolders(System.currentTimeMillis())
|
||||||
if (removedDeletedFolders > 0) {
|
val removedDeletedProfiles = SignalDatabase.notificationProfiles.removeStorageIdsFromOldDeletedProfiles(System.currentTimeMillis())
|
||||||
Log.i(TAG, "Removed $removedDeletedFolders folders from storage service that have been deleted for longer than 30 days.")
|
if (removedUnregistered > 0 || removedDeletedFolders > 0 || removedDeletedProfiles > 0) {
|
||||||
|
Log.i(TAG, "Removed $removedUnregistered unregistered, $removedDeletedFolders folders, $removedDeletedProfiles notification profiles from storage service that have been deleted for longer than 30 days.")
|
||||||
}
|
}
|
||||||
|
|
||||||
val localStorageIds = getAllLocalStorageIds(self)
|
val localStorageIds = getAllLocalStorageIds(self)
|
||||||
@@ -454,12 +456,14 @@ class StorageSyncJob private constructor(parameters: Parameters, private var loc
|
|||||||
StoryDistributionListRecordProcessor().process(records.storyDistributionLists, StorageSyncHelper.KEY_GENERATOR)
|
StoryDistributionListRecordProcessor().process(records.storyDistributionLists, StorageSyncHelper.KEY_GENERATOR)
|
||||||
CallLinkRecordProcessor().process(records.callLinkRecords, StorageSyncHelper.KEY_GENERATOR)
|
CallLinkRecordProcessor().process(records.callLinkRecords, StorageSyncHelper.KEY_GENERATOR)
|
||||||
ChatFolderRecordProcessor().process(records.chatFolderRecords, StorageSyncHelper.KEY_GENERATOR)
|
ChatFolderRecordProcessor().process(records.chatFolderRecords, StorageSyncHelper.KEY_GENERATOR)
|
||||||
|
NotificationProfileRecordProcessor().process(records.notificationProfileRecords, StorageSyncHelper.KEY_GENERATOR)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getAllLocalStorageIds(self: Recipient): List<StorageId> {
|
private fun getAllLocalStorageIds(self: Recipient): List<StorageId> {
|
||||||
return SignalDatabase.recipients.getContactStorageSyncIds() +
|
return SignalDatabase.recipients.getContactStorageSyncIds() +
|
||||||
listOf(StorageId.forAccount(self.storageId)) +
|
listOf(StorageId.forAccount(self.storageId)) +
|
||||||
SignalDatabase.chatFolders.getStorageSyncIds() +
|
SignalDatabase.chatFolders.getStorageSyncIds() +
|
||||||
|
SignalDatabase.notificationProfiles.getStorageSyncIds() +
|
||||||
SignalDatabase.unknownStorageIds.allUnknownIds
|
SignalDatabase.unknownStorageIds.allUnknownIds
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -533,6 +537,16 @@ class StorageSyncJob private constructor(parameters: Parameters, private var loc
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ManifestRecord.Identifier.Type.NOTIFICATION_PROFILE -> {
|
||||||
|
val query = SqlUtil.buildQuery("${NotificationProfileTables.NotificationProfileTable.STORAGE_SERVICE_ID} = ?", Base64.encodeWithPadding(id.raw))
|
||||||
|
val notificationProfile = SignalDatabase.notificationProfiles.getProfile(query)
|
||||||
|
if (notificationProfile?.notificationProfileId != null) {
|
||||||
|
records.add(StorageSyncModels.localToRemoteRecord(notificationProfile, id.raw))
|
||||||
|
} else {
|
||||||
|
throw MissingNotificationProfileModelError("Missing local notification profile model! Type: " + id.type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
val unknown = SignalDatabase.unknownStorageIds.getById(id.raw)
|
val unknown = SignalDatabase.unknownStorageIds.getById(id.raw)
|
||||||
if (unknown != null) {
|
if (unknown != null) {
|
||||||
@@ -567,6 +581,7 @@ class StorageSyncJob private constructor(parameters: Parameters, private var loc
|
|||||||
val storyDistributionLists: MutableList<SignalStoryDistributionListRecord> = mutableListOf()
|
val storyDistributionLists: MutableList<SignalStoryDistributionListRecord> = mutableListOf()
|
||||||
val callLinkRecords: MutableList<SignalCallLinkRecord> = mutableListOf()
|
val callLinkRecords: MutableList<SignalCallLinkRecord> = mutableListOf()
|
||||||
val chatFolderRecords: MutableList<SignalChatFolderRecord> = mutableListOf()
|
val chatFolderRecords: MutableList<SignalChatFolderRecord> = mutableListOf()
|
||||||
|
val notificationProfileRecords: MutableList<SignalNotificationProfileRecord> = mutableListOf()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
for (record in records) {
|
for (record in records) {
|
||||||
@@ -584,6 +599,8 @@ class StorageSyncJob private constructor(parameters: Parameters, private var loc
|
|||||||
callLinkRecords += record.proto.callLink!!.toSignalCallLinkRecord(record.id)
|
callLinkRecords += record.proto.callLink!!.toSignalCallLinkRecord(record.id)
|
||||||
} else if (record.proto.chatFolder != null) {
|
} else if (record.proto.chatFolder != null) {
|
||||||
chatFolderRecords += record.proto.chatFolder!!.toSignalChatFolderRecord(record.id)
|
chatFolderRecords += record.proto.chatFolder!!.toSignalChatFolderRecord(record.id)
|
||||||
|
} else if (record.proto.notificationProfile != null) {
|
||||||
|
notificationProfileRecords += record.proto.notificationProfile!!.toSignalNotificationProfileRecord(record.id)
|
||||||
} else if (record.id.isUnknown) {
|
} else if (record.id.isUnknown) {
|
||||||
unknown += record
|
unknown += record
|
||||||
} else {
|
} else {
|
||||||
@@ -599,6 +616,8 @@ class StorageSyncJob private constructor(parameters: Parameters, private var loc
|
|||||||
|
|
||||||
private class MissingChatFolderModelError(message: String?) : Error(message)
|
private class MissingChatFolderModelError(message: String?) : Error(message)
|
||||||
|
|
||||||
|
private class MissingNotificationProfileModelError(message: String?) : Error(message)
|
||||||
|
|
||||||
private class MissingUnknownModelError(message: String?) : Error(message)
|
private class MissingUnknownModelError(message: String?) : Error(message)
|
||||||
|
|
||||||
class Factory : Job.Factory<StorageSyncJob?> {
|
class Factory : Job.Factory<StorageSyncJob?> {
|
||||||
|
|||||||
@@ -2,20 +2,28 @@ package org.thoughtcrime.securesms.notifications.profiles
|
|||||||
|
|
||||||
import org.thoughtcrime.securesms.conversation.colors.AvatarColor
|
import org.thoughtcrime.securesms.conversation.colors.AvatarColor
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||||
|
import org.whispersystems.signalservice.api.storage.StorageId
|
||||||
|
|
||||||
data class NotificationProfile(
|
data class NotificationProfile(
|
||||||
val id: Long,
|
val id: Long,
|
||||||
val name: String,
|
val name: String,
|
||||||
val emoji: String,
|
val emoji: String,
|
||||||
val color: AvatarColor = AvatarColor.A210,
|
val color: AvatarColor = DEFAULT_NOTIFICATION_PROFILE_COLOR,
|
||||||
val createdAt: Long,
|
val createdAt: Long,
|
||||||
val allowAllCalls: Boolean = true,
|
val allowAllCalls: Boolean = true,
|
||||||
val allowAllMentions: Boolean = false,
|
val allowAllMentions: Boolean = false,
|
||||||
val schedule: NotificationProfileSchedule,
|
val schedule: NotificationProfileSchedule,
|
||||||
val allowedMembers: Set<RecipientId> = emptySet(),
|
val allowedMembers: Set<RecipientId> = emptySet(),
|
||||||
val notificationProfileId: NotificationProfileId
|
val notificationProfileId: NotificationProfileId,
|
||||||
|
val deletedTimestampMs: Long = 0,
|
||||||
|
val storageServiceId: StorageId? = null,
|
||||||
|
val storageServiceProto: ByteArray? = null
|
||||||
) : Comparable<NotificationProfile> {
|
) : Comparable<NotificationProfile> {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val DEFAULT_NOTIFICATION_PROFILE_COLOR = AvatarColor.A210
|
||||||
|
}
|
||||||
|
|
||||||
fun isRecipientAllowed(id: RecipientId): Boolean {
|
fun isRecipientAllowed(id: RecipientId): Boolean {
|
||||||
return allowedMembers.contains(id)
|
return allowedMembers.contains(id)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import okio.ByteString
|
|||||||
import org.signal.core.util.isNotEmpty
|
import org.signal.core.util.isNotEmpty
|
||||||
import org.signal.core.util.logging.Log
|
import org.signal.core.util.logging.Log
|
||||||
import org.signal.core.util.nullIfEmpty
|
import org.signal.core.util.nullIfEmpty
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper.applyAccountStorageSyncUpdates
|
import org.thoughtcrime.securesms.storage.StorageSyncHelper.applyAccountStorageSyncUpdates
|
||||||
import org.whispersystems.signalservice.api.storage.IAPSubscriptionId
|
import org.whispersystems.signalservice.api.storage.IAPSubscriptionId
|
||||||
@@ -125,7 +124,6 @@ class AccountRecordProcessor(
|
|||||||
preferContactAvatars = remote.proto.preferContactAvatars
|
preferContactAvatars = remote.proto.preferContactAvatars
|
||||||
universalExpireTimer = remote.proto.universalExpireTimer
|
universalExpireTimer = remote.proto.universalExpireTimer
|
||||||
primarySendsSms = false
|
primarySendsSms = false
|
||||||
e164 = if (SignalStore.account.isPrimaryDevice) local.proto.e164 else remote.proto.e164
|
|
||||||
preferredReactionEmoji = remote.proto.preferredReactionEmoji.takeIf { it.isNotEmpty() } ?: local.proto.preferredReactionEmoji
|
preferredReactionEmoji = remote.proto.preferredReactionEmoji.takeIf { it.isNotEmpty() } ?: local.proto.preferredReactionEmoji
|
||||||
displayBadgesOnProfile = remote.proto.displayBadgesOnProfile
|
displayBadgesOnProfile = remote.proto.displayBadgesOnProfile
|
||||||
subscriptionManuallyCancelled = remote.proto.subscriptionManuallyCancelled
|
subscriptionManuallyCancelled = remote.proto.subscriptionManuallyCancelled
|
||||||
@@ -138,6 +136,7 @@ class AccountRecordProcessor(
|
|||||||
hasCompletedUsernameOnboarding = remote.proto.hasCompletedUsernameOnboarding || local.proto.hasCompletedUsernameOnboarding
|
hasCompletedUsernameOnboarding = remote.proto.hasCompletedUsernameOnboarding || local.proto.hasCompletedUsernameOnboarding
|
||||||
username = remote.proto.username
|
username = remote.proto.username
|
||||||
usernameLink = remote.proto.usernameLink
|
usernameLink = remote.proto.usernameLink
|
||||||
|
notificationProfileManualOverride = remote.proto.notificationProfileManualOverride
|
||||||
|
|
||||||
safeSetPayments(payments?.enabled == true, payments?.entropy?.toByteArray())
|
safeSetPayments(payments?.enabled == true, payments?.entropy?.toByteArray())
|
||||||
safeSetSubscriber(donationSubscriberId, donationSubscriberCurrencyCode)
|
safeSetSubscriber(donationSubscriberId, donationSubscriberCurrencyCode)
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import org.whispersystems.signalservice.api.storage.StorageId
|
|||||||
import org.whispersystems.signalservice.api.util.OptionalUtil.asOptional
|
import org.whispersystems.signalservice.api.util.OptionalUtil.asOptional
|
||||||
import org.whispersystems.signalservice.api.util.UuidUtil
|
import org.whispersystems.signalservice.api.util.UuidUtil
|
||||||
import org.whispersystems.signalservice.internal.storage.protos.ChatFolderRecord
|
import org.whispersystems.signalservice.internal.storage.protos.ChatFolderRecord
|
||||||
|
import org.whispersystems.signalservice.internal.storage.protos.Recipient
|
||||||
import java.util.Optional
|
import java.util.Optional
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
@@ -96,7 +97,7 @@ class ChatFolderRecordProcessor : DefaultStorageRecordProcessor<SignalChatFolder
|
|||||||
SignalDatabase.chatFolders.updateChatFolderFromStorageSync(update.new)
|
SignalDatabase.chatFolders.updateChatFolderFromStorageSync(update.new)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun containsInvalidServiceId(recipients: List<ChatFolderRecord.Recipient>): Boolean {
|
private fun containsInvalidServiceId(recipients: List<Recipient>): Boolean {
|
||||||
return recipients.any { recipient ->
|
return recipients.any { recipient ->
|
||||||
recipient.contact != null && ServiceId.parseOrNull(recipient.contact!!.serviceId) == null
|
recipient.contact != null && ServiceId.parseOrNull(recipient.contact!!.serviceId) == null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
package org.thoughtcrime.securesms.storage
|
||||||
|
|
||||||
|
import org.signal.core.util.SqlUtil
|
||||||
|
import org.signal.core.util.logging.Log
|
||||||
|
import org.thoughtcrime.securesms.database.NotificationProfileTables
|
||||||
|
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||||
|
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfileId
|
||||||
|
import org.whispersystems.signalservice.api.push.ServiceId
|
||||||
|
import org.whispersystems.signalservice.api.storage.SignalNotificationProfileRecord
|
||||||
|
import org.whispersystems.signalservice.api.storage.StorageId
|
||||||
|
import org.whispersystems.signalservice.api.util.OptionalUtil.asOptional
|
||||||
|
import org.whispersystems.signalservice.api.util.UuidUtil
|
||||||
|
import org.whispersystems.signalservice.internal.storage.protos.Recipient
|
||||||
|
import java.util.Optional
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Record processor for [SignalNotificationProfileRecord].
|
||||||
|
* Handles merging and updating our local store when processing remote notification profile storage records.
|
||||||
|
*/
|
||||||
|
class NotificationProfileRecordProcessor : DefaultStorageRecordProcessor<SignalNotificationProfileRecord>() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = Log.tag(NotificationProfileRecordProcessor::class)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun compare(o1: SignalNotificationProfileRecord, o2: SignalNotificationProfileRecord): Int {
|
||||||
|
return if (o1.proto.id == o2.proto.id) {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notification profiles must have a valid identifier
|
||||||
|
* Notification profiles must have a name
|
||||||
|
* All allowed members must have a valid serviceId
|
||||||
|
*/
|
||||||
|
override fun isInvalid(remote: SignalNotificationProfileRecord): Boolean {
|
||||||
|
return UuidUtil.parseOrNull(remote.proto.id) == null ||
|
||||||
|
remote.proto.name.isEmpty() ||
|
||||||
|
containsInvalidServiceId(remote.proto.allowedMembers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getMatching(remote: SignalNotificationProfileRecord, keyGenerator: StorageKeyGenerator): Optional<SignalNotificationProfileRecord> {
|
||||||
|
Log.d(TAG, "Attempting to get matching record...")
|
||||||
|
val uuid: UUID = UuidUtil.parseOrThrow(remote.proto.id)
|
||||||
|
val query = SqlUtil.buildQuery("${NotificationProfileTables.NotificationProfileTable.NOTIFICATION_PROFILE_ID} = ?", NotificationProfileId(uuid))
|
||||||
|
|
||||||
|
val notificationProfile = SignalDatabase.notificationProfiles.getProfile(query)
|
||||||
|
|
||||||
|
return if (notificationProfile?.storageServiceId != null) {
|
||||||
|
StorageSyncModels.localToRemoteNotificationProfile(notificationProfile, notificationProfile.storageServiceId.raw).asOptional()
|
||||||
|
} else if (notificationProfile != null) {
|
||||||
|
Log.d(TAG, "Notification profile was missing a storage service id, generating one")
|
||||||
|
val storageId = StorageId.forNotificationProfile(keyGenerator.generate())
|
||||||
|
SignalDatabase.notificationProfiles.applyStorageIdUpdate(notificationProfile.notificationProfileId, storageId)
|
||||||
|
StorageSyncModels.localToRemoteNotificationProfile(notificationProfile, storageId.raw).asOptional()
|
||||||
|
} else {
|
||||||
|
Log.d(TAG, "Could not find a matching record. Returning an empty.")
|
||||||
|
Optional.empty<SignalNotificationProfileRecord>()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A deleted record takes precedence over a non-deleted record
|
||||||
|
* while an earlier deletion takes precedence over a later deletion
|
||||||
|
*/
|
||||||
|
override fun merge(remote: SignalNotificationProfileRecord, local: SignalNotificationProfileRecord, keyGenerator: StorageKeyGenerator): SignalNotificationProfileRecord {
|
||||||
|
val isRemoteDeleted = remote.proto.deletedAtTimestampMs > 0
|
||||||
|
val isLocalDeleted = local.proto.deletedAtTimestampMs > 0
|
||||||
|
|
||||||
|
return when {
|
||||||
|
isRemoteDeleted && isLocalDeleted -> if (remote.proto.deletedAtTimestampMs < local.proto.deletedAtTimestampMs) remote else local
|
||||||
|
isRemoteDeleted -> remote
|
||||||
|
isLocalDeleted -> local
|
||||||
|
else -> remote
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun insertLocal(record: SignalNotificationProfileRecord) {
|
||||||
|
SignalDatabase.notificationProfiles.insertNotificationProfileFromStorageSync(record)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun updateLocal(update: StorageRecordUpdate<SignalNotificationProfileRecord>) {
|
||||||
|
SignalDatabase.notificationProfiles.updateNotificationProfileFromStorageSync(update.new)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun containsInvalidServiceId(recipients: List<Recipient>): Boolean {
|
||||||
|
return recipients.any { recipient ->
|
||||||
|
recipient.contact != null && ServiceId.parseOrNull(recipient.contact!!.serviceId) == null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,10 +5,12 @@ import androidx.annotation.VisibleForTesting
|
|||||||
import okio.ByteString
|
import okio.ByteString
|
||||||
import okio.ByteString.Companion.toByteString
|
import okio.ByteString.Companion.toByteString
|
||||||
import org.signal.core.util.Base64.encodeWithPadding
|
import org.signal.core.util.Base64.encodeWithPadding
|
||||||
|
import org.signal.core.util.SqlUtil
|
||||||
import org.signal.core.util.logging.Log
|
import org.signal.core.util.logging.Log
|
||||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository.getSubscriber
|
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository.getSubscriber
|
||||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository.isUserManuallyCancelled
|
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository.isUserManuallyCancelled
|
||||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository.setSubscriber
|
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository.setSubscriber
|
||||||
|
import org.thoughtcrime.securesms.database.NotificationProfileTables
|
||||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||||
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
|
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
|
||||||
import org.thoughtcrime.securesms.database.model.RecipientRecord
|
import org.thoughtcrime.securesms.database.model.RecipientRecord
|
||||||
@@ -18,6 +20,7 @@ import org.thoughtcrime.securesms.jobs.StorageSyncJob
|
|||||||
import org.thoughtcrime.securesms.keyvalue.AccountValues
|
import org.thoughtcrime.securesms.keyvalue.AccountValues
|
||||||
import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues.PhoneNumberDiscoverabilityMode
|
import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues.PhoneNumberDiscoverabilityMode
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
|
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfileId
|
||||||
import org.thoughtcrime.securesms.payments.Entropy
|
import org.thoughtcrime.securesms.payments.Entropy
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient.Companion.self
|
import org.thoughtcrime.securesms.recipients.Recipient.Companion.self
|
||||||
@@ -171,6 +174,7 @@ object StorageSyncHelper {
|
|||||||
color = StorageSyncModels.localToRemoteUsernameColor(SignalStore.misc.usernameQrCodeColorScheme)
|
color = StorageSyncModels.localToRemoteUsernameColor(SignalStore.misc.usernameQrCodeColorScheme)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
notificationProfileManualOverride = getNotificationProfileManualOverride()
|
||||||
|
|
||||||
getSubscriber(InAppPaymentSubscriberRecord.Type.DONATION)?.let {
|
getSubscriber(InAppPaymentSubscriberRecord.Type.DONATION)?.let {
|
||||||
safeSetSubscriber(it.subscriberId.bytes.toByteString(), it.currency?.currencyCode ?: "")
|
safeSetSubscriber(it.subscriberId.bytes.toByteString(), it.currency?.currencyCode ?: "")
|
||||||
@@ -186,6 +190,24 @@ object StorageSyncHelper {
|
|||||||
return accountRecord.toSignalAccountRecord(StorageId.forAccount(storageId)).toSignalStorageRecord()
|
return accountRecord.toSignalAccountRecord(StorageId.forAccount(storageId)).toSignalStorageRecord()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getNotificationProfileManualOverride(): AccountRecord.NotificationProfileManualOverride {
|
||||||
|
val profile = SignalDatabase.notificationProfiles.getProfile(SignalStore.notificationProfile.manuallyEnabledProfile)
|
||||||
|
return if (profile != null && profile.deletedTimestampMs == 0L) {
|
||||||
|
// From [StorageService.proto], end timestamp should be unset if no timespan was chosen in the UI
|
||||||
|
val endTimestamp = if (SignalStore.notificationProfile.manuallyEnabledUntil == Long.MAX_VALUE) 0 else SignalStore.notificationProfile.manuallyEnabledUntil
|
||||||
|
AccountRecord.NotificationProfileManualOverride(
|
||||||
|
enabled = AccountRecord.NotificationProfileManualOverride.ManuallyEnabled(
|
||||||
|
id = UuidUtil.toByteArray(profile.notificationProfileId.uuid).toByteString(),
|
||||||
|
endAtTimestampMs = endTimestamp
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
AccountRecord.NotificationProfileManualOverride(
|
||||||
|
disabledAtTimestampMs = SignalStore.notificationProfile.manuallyDisabledAt
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun applyAccountStorageSyncUpdates(context: Context, self: Recipient, updatedRecord: SignalAccountRecord, fetchProfile: Boolean) {
|
fun applyAccountStorageSyncUpdates(context: Context, self: Recipient, updatedRecord: SignalAccountRecord, fetchProfile: Boolean) {
|
||||||
val localRecord = buildAccountRecord(context, self).let { it.proto.account!!.toSignalAccountRecord(it.id) }
|
val localRecord = buildAccountRecord(context, self).let { it.proto.account!!.toSignalAccountRecord(it.id) }
|
||||||
@@ -252,6 +274,33 @@ object StorageSyncHelper {
|
|||||||
|
|
||||||
SignalStore.misc.usernameQrCodeColorScheme = StorageSyncModels.remoteToLocalUsernameColor(update.new.proto.usernameLink!!.color)
|
SignalStore.misc.usernameQrCodeColorScheme = StorageSyncModels.remoteToLocalUsernameColor(update.new.proto.usernameLink!!.color)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (update.new.proto.notificationProfileManualOverride != null) {
|
||||||
|
if (update.new.proto.notificationProfileManualOverride!!.enabled != null) {
|
||||||
|
val remoteProfile = update.new.proto.notificationProfileManualOverride!!.enabled!!
|
||||||
|
val remoteId = UuidUtil.parseOrNull(remoteProfile.id)
|
||||||
|
val remoteEndTime = if (remoteProfile.endAtTimestampMs == 0L) Long.MAX_VALUE else remoteProfile.endAtTimestampMs
|
||||||
|
|
||||||
|
if (remoteId == null) {
|
||||||
|
Log.w(TAG, "Remote notification profile id is not valid")
|
||||||
|
} else {
|
||||||
|
val query = SqlUtil.buildQuery("${NotificationProfileTables.NotificationProfileTable.NOTIFICATION_PROFILE_ID} = ?", NotificationProfileId(remoteId))
|
||||||
|
val localProfile = SignalDatabase.notificationProfiles.getProfile(query)
|
||||||
|
|
||||||
|
if (localProfile == null) {
|
||||||
|
Log.w(TAG, "Unable to find local notification profile with given remote id")
|
||||||
|
} else {
|
||||||
|
SignalStore.notificationProfile.manuallyEnabledProfile = localProfile.id
|
||||||
|
SignalStore.notificationProfile.manuallyEnabledUntil = remoteEndTime
|
||||||
|
SignalStore.notificationProfile.manuallyDisabledAt = System.currentTimeMillis()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
SignalStore.notificationProfile.manuallyEnabledProfile = 0
|
||||||
|
SignalStore.notificationProfile.manuallyEnabledUntil = 0
|
||||||
|
SignalStore.notificationProfile.manuallyDisabledAt = update.new.proto.notificationProfileManualOverride!!.disabledAtTimestampMs!!
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import okio.ByteString
|
|||||||
import okio.ByteString.Companion.toByteString
|
import okio.ByteString.Companion.toByteString
|
||||||
import org.signal.core.util.isNotEmpty
|
import org.signal.core.util.isNotEmpty
|
||||||
import org.signal.core.util.isNullOrEmpty
|
import org.signal.core.util.isNullOrEmpty
|
||||||
|
import org.signal.core.util.logging.Log
|
||||||
|
import org.signal.libsignal.zkgroup.InvalidInputException
|
||||||
import org.signal.libsignal.zkgroup.groups.GroupMasterKey
|
import org.signal.libsignal.zkgroup.groups.GroupMasterKey
|
||||||
import org.thoughtcrime.securesms.components.settings.app.chats.folders.ChatFolderRecord
|
import org.thoughtcrime.securesms.components.settings.app.chats.folders.ChatFolderRecord
|
||||||
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.UsernameQrCodeColorScheme
|
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.UsernameQrCodeColorScheme
|
||||||
@@ -20,14 +22,21 @@ import org.thoughtcrime.securesms.database.SignalDatabase.Companion.inAppPayment
|
|||||||
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
|
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
|
||||||
import org.thoughtcrime.securesms.database.model.RecipientRecord
|
import org.thoughtcrime.securesms.database.model.RecipientRecord
|
||||||
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
|
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
|
||||||
|
import org.thoughtcrime.securesms.groups.BadGroupIdException
|
||||||
|
import org.thoughtcrime.securesms.groups.GroupId
|
||||||
import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues
|
import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues
|
||||||
|
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
|
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||||
|
import org.whispersystems.signalservice.api.push.ServiceId
|
||||||
|
import org.whispersystems.signalservice.api.push.SignalServiceAddress
|
||||||
import org.whispersystems.signalservice.api.storage.IAPSubscriptionId
|
import org.whispersystems.signalservice.api.storage.IAPSubscriptionId
|
||||||
import org.whispersystems.signalservice.api.storage.SignalCallLinkRecord
|
import org.whispersystems.signalservice.api.storage.SignalCallLinkRecord
|
||||||
import org.whispersystems.signalservice.api.storage.SignalChatFolderRecord
|
import org.whispersystems.signalservice.api.storage.SignalChatFolderRecord
|
||||||
import org.whispersystems.signalservice.api.storage.SignalContactRecord
|
import org.whispersystems.signalservice.api.storage.SignalContactRecord
|
||||||
import org.whispersystems.signalservice.api.storage.SignalGroupV1Record
|
import org.whispersystems.signalservice.api.storage.SignalGroupV1Record
|
||||||
import org.whispersystems.signalservice.api.storage.SignalGroupV2Record
|
import org.whispersystems.signalservice.api.storage.SignalGroupV2Record
|
||||||
|
import org.whispersystems.signalservice.api.storage.SignalNotificationProfileRecord
|
||||||
import org.whispersystems.signalservice.api.storage.SignalStorageRecord
|
import org.whispersystems.signalservice.api.storage.SignalStorageRecord
|
||||||
import org.whispersystems.signalservice.api.storage.SignalStoryDistributionListRecord
|
import org.whispersystems.signalservice.api.storage.SignalStoryDistributionListRecord
|
||||||
import org.whispersystems.signalservice.api.storage.StorageId
|
import org.whispersystems.signalservice.api.storage.StorageId
|
||||||
@@ -36,6 +45,7 @@ import org.whispersystems.signalservice.api.storage.toSignalChatFolderRecord
|
|||||||
import org.whispersystems.signalservice.api.storage.toSignalContactRecord
|
import org.whispersystems.signalservice.api.storage.toSignalContactRecord
|
||||||
import org.whispersystems.signalservice.api.storage.toSignalGroupV1Record
|
import org.whispersystems.signalservice.api.storage.toSignalGroupV1Record
|
||||||
import org.whispersystems.signalservice.api.storage.toSignalGroupV2Record
|
import org.whispersystems.signalservice.api.storage.toSignalGroupV2Record
|
||||||
|
import org.whispersystems.signalservice.api.storage.toSignalNotificationProfileRecord
|
||||||
import org.whispersystems.signalservice.api.storage.toSignalStorageRecord
|
import org.whispersystems.signalservice.api.storage.toSignalStorageRecord
|
||||||
import org.whispersystems.signalservice.api.storage.toSignalStoryDistributionListRecord
|
import org.whispersystems.signalservice.api.storage.toSignalStoryDistributionListRecord
|
||||||
import org.whispersystems.signalservice.api.subscriptions.SubscriberId
|
import org.whispersystems.signalservice.api.subscriptions.SubscriberId
|
||||||
@@ -44,13 +54,18 @@ import org.whispersystems.signalservice.internal.storage.protos.AccountRecord
|
|||||||
import org.whispersystems.signalservice.internal.storage.protos.ContactRecord
|
import org.whispersystems.signalservice.internal.storage.protos.ContactRecord
|
||||||
import org.whispersystems.signalservice.internal.storage.protos.ContactRecord.IdentityState
|
import org.whispersystems.signalservice.internal.storage.protos.ContactRecord.IdentityState
|
||||||
import org.whispersystems.signalservice.internal.storage.protos.GroupV2Record
|
import org.whispersystems.signalservice.internal.storage.protos.GroupV2Record
|
||||||
|
import java.time.DayOfWeek
|
||||||
import java.util.Currency
|
import java.util.Currency
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
import org.whispersystems.signalservice.internal.storage.protos.AvatarColor as RemoteAvatarColor
|
import org.whispersystems.signalservice.internal.storage.protos.AvatarColor as RemoteAvatarColor
|
||||||
import org.whispersystems.signalservice.internal.storage.protos.ChatFolderRecord as RemoteChatFolder
|
import org.whispersystems.signalservice.internal.storage.protos.ChatFolderRecord as RemoteChatFolder
|
||||||
|
import org.whispersystems.signalservice.internal.storage.protos.NotificationProfile.DayOfWeek as RemoteDayOfWeek
|
||||||
|
import org.whispersystems.signalservice.internal.storage.protos.Recipient as RemoteRecipient
|
||||||
|
|
||||||
object StorageSyncModels {
|
object StorageSyncModels {
|
||||||
|
|
||||||
|
private val TAG = Log.tag(StorageSyncModels::class.java)
|
||||||
|
|
||||||
fun localToRemoteRecord(settings: RecipientRecord): SignalStorageRecord {
|
fun localToRemoteRecord(settings: RecipientRecord): SignalStorageRecord {
|
||||||
if (settings.storageId == null) {
|
if (settings.storageId == null) {
|
||||||
throw AssertionError("Must have a storage key!")
|
throw AssertionError("Must have a storage key!")
|
||||||
@@ -82,6 +97,10 @@ object StorageSyncModels {
|
|||||||
return localToRemoteChatFolder(folder, rawStorageId).toSignalStorageRecord()
|
return localToRemoteChatFolder(folder, rawStorageId).toSignalStorageRecord()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun localToRemoteRecord(profile: NotificationProfile, rawStorageId: ByteArray): SignalStorageRecord {
|
||||||
|
return localToRemoteNotificationProfile(profile, rawStorageId).toSignalStorageRecord()
|
||||||
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun localToRemotePhoneNumberSharingMode(phoneNumberPhoneNumberSharingMode: PhoneNumberPrivacyValues.PhoneNumberSharingMode): AccountRecord.PhoneNumberSharingMode {
|
fun localToRemotePhoneNumberSharingMode(phoneNumberPhoneNumberSharingMode: PhoneNumberPrivacyValues.PhoneNumberSharingMode): AccountRecord.PhoneNumberSharingMode {
|
||||||
return when (phoneNumberPhoneNumberSharingMode) {
|
return when (phoneNumberPhoneNumberSharingMode) {
|
||||||
@@ -396,22 +415,96 @@ object StorageSyncModels {
|
|||||||
}.build().toSignalChatFolderRecord(StorageId.forChatFolder(rawStorageId))
|
}.build().toSignalChatFolderRecord(StorageId.forChatFolder(rawStorageId))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun localToRemoteChatFolderRecipients(threadIds: List<Long>): List<RemoteChatFolder.Recipient> {
|
fun localToRemoteNotificationProfile(profile: NotificationProfile, rawStorageId: ByteArray?): SignalNotificationProfileRecord {
|
||||||
|
return SignalNotificationProfileRecord.newBuilder(profile.storageServiceProto).apply {
|
||||||
|
id = UuidUtil.toByteArray(profile.notificationProfileId.uuid).toByteString()
|
||||||
|
name = profile.name
|
||||||
|
emoji = profile.emoji
|
||||||
|
color = profile.color.colorInt()
|
||||||
|
createdAtMs = profile.createdAt
|
||||||
|
allowAllCalls = profile.allowAllCalls
|
||||||
|
allowAllMentions = profile.allowAllMentions
|
||||||
|
allowedMembers = localToRemoteRecipients(profile.allowedMembers.toList())
|
||||||
|
scheduleEnabled = profile.schedule.enabled
|
||||||
|
scheduleStartTime = profile.schedule.start
|
||||||
|
scheduleEndTime = profile.schedule.end
|
||||||
|
scheduleDaysEnabled = localToRemoteDayOfWeek(profile.schedule.daysEnabled)
|
||||||
|
deletedAtTimestampMs = profile.deletedTimestampMs
|
||||||
|
}.build().toSignalNotificationProfileRecord(StorageId.forNotificationProfile(rawStorageId))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun localToRemoteDayOfWeek(daysEnabled: Set<DayOfWeek>): List<RemoteDayOfWeek> {
|
||||||
|
return daysEnabled.map { day ->
|
||||||
|
when (day) {
|
||||||
|
DayOfWeek.MONDAY -> RemoteDayOfWeek.MONDAY
|
||||||
|
DayOfWeek.TUESDAY -> RemoteDayOfWeek.TUESDAY
|
||||||
|
DayOfWeek.WEDNESDAY -> RemoteDayOfWeek.WEDNESDAY
|
||||||
|
DayOfWeek.THURSDAY -> RemoteDayOfWeek.THURSDAY
|
||||||
|
DayOfWeek.FRIDAY -> RemoteDayOfWeek.FRIDAY
|
||||||
|
DayOfWeek.SATURDAY -> RemoteDayOfWeek.SATURDAY
|
||||||
|
DayOfWeek.SUNDAY -> RemoteDayOfWeek.SUNDAY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun RemoteDayOfWeek.toLocal(): DayOfWeek {
|
||||||
|
return when (this) {
|
||||||
|
RemoteDayOfWeek.UNKNOWN -> DayOfWeek.MONDAY
|
||||||
|
RemoteDayOfWeek.MONDAY -> DayOfWeek.MONDAY
|
||||||
|
RemoteDayOfWeek.TUESDAY -> DayOfWeek.TUESDAY
|
||||||
|
RemoteDayOfWeek.WEDNESDAY -> DayOfWeek.WEDNESDAY
|
||||||
|
RemoteDayOfWeek.THURSDAY -> DayOfWeek.THURSDAY
|
||||||
|
RemoteDayOfWeek.FRIDAY -> DayOfWeek.FRIDAY
|
||||||
|
RemoteDayOfWeek.SATURDAY -> DayOfWeek.SATURDAY
|
||||||
|
RemoteDayOfWeek.SUNDAY -> DayOfWeek.SUNDAY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun localToRemoteChatFolderRecipients(threadIds: List<Long>): List<RemoteRecipient> {
|
||||||
val recipientIds = SignalDatabase.threads.getRecipientIdsForThreadIds(threadIds)
|
val recipientIds = SignalDatabase.threads.getRecipientIdsForThreadIds(threadIds)
|
||||||
|
return localToRemoteRecipients(recipientIds)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun localToRemoteRecipients(recipientIds: List<RecipientId>): List<RemoteRecipient> {
|
||||||
return recipientIds.mapNotNull { id ->
|
return recipientIds.mapNotNull { id ->
|
||||||
val recipient = SignalDatabase.recipients.getRecordForSync(id) ?: throw AssertionError("Missing recipient for id")
|
val recipient = SignalDatabase.recipients.getRecordForSync(id) ?: throw AssertionError("Missing recipient for id")
|
||||||
when (recipient.recipientType) {
|
when (recipient.recipientType) {
|
||||||
RecipientType.INDIVIDUAL -> {
|
RecipientType.INDIVIDUAL -> {
|
||||||
RemoteChatFolder.Recipient(contact = RemoteChatFolder.Recipient.Contact(serviceId = recipient.serviceId?.toString() ?: "", e164 = recipient.e164 ?: ""))
|
RemoteRecipient(contact = RemoteRecipient.Contact(serviceId = recipient.serviceId?.toString() ?: "", e164 = recipient.e164 ?: ""))
|
||||||
}
|
}
|
||||||
RecipientType.GV1 -> {
|
RecipientType.GV1 -> {
|
||||||
RemoteChatFolder.Recipient(legacyGroupId = recipient.groupId!!.requireV1().decodedId.toByteString())
|
RemoteRecipient(legacyGroupId = recipient.groupId!!.requireV1().decodedId.toByteString())
|
||||||
}
|
}
|
||||||
RecipientType.GV2 -> {
|
RecipientType.GV2 -> {
|
||||||
RemoteChatFolder.Recipient(groupMasterKey = recipient.syncExtras.groupMasterKey!!.serialize().toByteString())
|
RemoteRecipient(groupMasterKey = recipient.syncExtras.groupMasterKey!!.serialize().toByteString())
|
||||||
}
|
}
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun remoteToLocalRecipient(remoteRecipient: RemoteRecipient): Recipient? {
|
||||||
|
return if (remoteRecipient.contact != null) {
|
||||||
|
val serviceId = ServiceId.parseOrNull(remoteRecipient.contact!!.serviceId)
|
||||||
|
val e164 = remoteRecipient.contact!!.e164
|
||||||
|
Recipient.externalPush(SignalServiceAddress(serviceId, e164))
|
||||||
|
} else if (remoteRecipient.legacyGroupId != null) {
|
||||||
|
try {
|
||||||
|
Recipient.externalGroupExact(GroupId.v1(remoteRecipient.legacyGroupId!!.toByteArray()))
|
||||||
|
} catch (e: BadGroupIdException) {
|
||||||
|
Log.w(TAG, "Failed to parse groupV1 ID!", e)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
} else if (remoteRecipient.groupMasterKey != null) {
|
||||||
|
try {
|
||||||
|
Recipient.externalGroupExact(GroupId.v2(GroupMasterKey(remoteRecipient.groupMasterKey!!.toByteArray())))
|
||||||
|
} catch (e: InvalidInputException) {
|
||||||
|
Log.w(TAG, "Failed to parse groupV2 master key!", e)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "Could not find recipient")
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -156,6 +156,11 @@ public final class StorageSyncValidations {
|
|||||||
throw new DuplicateChatFolderError();
|
throw new DuplicateChatFolderError();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ids = manifest.getStorageIdsByType().get(ManifestRecord.Identifier.Type.NOTIFICATION_PROFILE.getValue());
|
||||||
|
if (ids.size() != new HashSet<>(ids).size()) {
|
||||||
|
throw new DuplicateNotificationProfileError();
|
||||||
|
}
|
||||||
|
|
||||||
throw new DuplicateRawIdAcrossTypesError();
|
throw new DuplicateRawIdAcrossTypesError();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,6 +222,9 @@ public final class StorageSyncValidations {
|
|||||||
private static final class DuplicateInsertInWriteError extends Error {
|
private static final class DuplicateInsertInWriteError extends Error {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static final class DuplicateNotificationProfileError extends Error {
|
||||||
|
}
|
||||||
|
|
||||||
private static final class InsertNotPresentInFullIdSetError extends Error {
|
private static final class InsertNotPresentInFullIdSetError extends Error {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -52,6 +52,9 @@ class StorageServicePlugin : Plugin {
|
|||||||
} else if (record.proto.chatFolder != null) {
|
} else if (record.proto.chatFolder != null) {
|
||||||
row += "Chat Folder"
|
row += "Chat Folder"
|
||||||
row += record.proto.chatFolder.toString()
|
row += record.proto.chatFolder.toString()
|
||||||
|
} else if (record.proto.notificationProfile != null) {
|
||||||
|
row += "Notification Profile"
|
||||||
|
row += record.proto.notificationProfile.toString()
|
||||||
} else {
|
} else {
|
||||||
row += "Unknown"
|
row += "Unknown"
|
||||||
row += ""
|
row += ""
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import org.whispersystems.signalservice.api.storage.SignalChatFolderRecord
|
|||||||
import org.whispersystems.signalservice.api.storage.StorageId
|
import org.whispersystems.signalservice.api.storage.StorageId
|
||||||
import org.whispersystems.signalservice.api.util.UuidUtil
|
import org.whispersystems.signalservice.api.util.UuidUtil
|
||||||
import org.whispersystems.signalservice.internal.storage.protos.ChatFolderRecord
|
import org.whispersystems.signalservice.internal.storage.protos.ChatFolderRecord
|
||||||
|
import org.whispersystems.signalservice.internal.storage.protos.Recipient
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -203,7 +204,7 @@ class ChatFolderRecordProcessorTest {
|
|||||||
includeAllGroupChats = false
|
includeAllGroupChats = false
|
||||||
folderType = ChatFolderRecord.FolderType.CUSTOM
|
folderType = ChatFolderRecord.FolderType.CUSTOM
|
||||||
deletedAtTimestampMs = 0L
|
deletedAtTimestampMs = 0L
|
||||||
includedRecipients = listOf(ChatFolderRecord.Recipient(contact = ChatFolderRecord.Recipient.Contact(serviceId = "bad")))
|
includedRecipients = listOf(Recipient(contact = Recipient.Contact(serviceId = "bad")))
|
||||||
}.build()
|
}.build()
|
||||||
val record = SignalChatFolderRecord(STORAGE_ID, proto)
|
val record = SignalChatFolderRecord(STORAGE_ID, proto)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,116 @@
|
|||||||
|
package org.thoughtcrime.securesms.storage
|
||||||
|
|
||||||
|
import okio.ByteString.Companion.toByteString
|
||||||
|
import org.junit.Assert.assertFalse
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.BeforeClass
|
||||||
|
import org.junit.Test
|
||||||
|
import org.signal.core.util.logging.Log
|
||||||
|
import org.thoughtcrime.securesms.testutil.EmptyLogger
|
||||||
|
import org.whispersystems.signalservice.api.storage.SignalNotificationProfileRecord
|
||||||
|
import org.whispersystems.signalservice.api.storage.StorageId
|
||||||
|
import org.whispersystems.signalservice.api.util.UuidUtil
|
||||||
|
import org.whispersystems.signalservice.internal.storage.protos.NotificationProfile
|
||||||
|
import org.whispersystems.signalservice.internal.storage.protos.Recipient
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for [NotificationProfileRecordProcessor]
|
||||||
|
*/
|
||||||
|
class NotificationProfileRecordProcessorTest {
|
||||||
|
companion object {
|
||||||
|
val STORAGE_ID: StorageId = StorageId.forNotificationProfile(byteArrayOf(1, 2, 3, 4))
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
@BeforeClass
|
||||||
|
fun setUpClass() {
|
||||||
|
Log.initialize(EmptyLogger())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val testSubject = NotificationProfileRecordProcessor()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Given a valid proto with a known name and id, assert valid`() {
|
||||||
|
// GIVEN
|
||||||
|
val proto = NotificationProfile.Builder().apply {
|
||||||
|
id = UuidUtil.toByteArray(UUID.randomUUID()).toByteString()
|
||||||
|
name = "name"
|
||||||
|
}.build()
|
||||||
|
val record = SignalNotificationProfileRecord(STORAGE_ID, proto)
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
val result = testSubject.isInvalid(record)
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assertFalse(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Given a valid proto with a deleted timestamp, known name and id, assert valid`() {
|
||||||
|
// GIVEN
|
||||||
|
val proto = NotificationProfile.Builder().apply {
|
||||||
|
id = UuidUtil.toByteArray(UUID.randomUUID()).toByteString()
|
||||||
|
name = "name"
|
||||||
|
deletedAtTimestampMs = 1000L
|
||||||
|
}.build()
|
||||||
|
val record = SignalNotificationProfileRecord(STORAGE_ID, proto)
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
val result = testSubject.isInvalid(record)
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assertFalse(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Given an invalid proto with no id, assert invalid`() {
|
||||||
|
// GIVEN
|
||||||
|
val proto = NotificationProfile.Builder().apply {
|
||||||
|
id = "Bad".toByteArray().toByteString()
|
||||||
|
name = "Profile"
|
||||||
|
deletedAtTimestampMs = 0L
|
||||||
|
}.build()
|
||||||
|
val record = SignalNotificationProfileRecord(STORAGE_ID, proto)
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
val result = testSubject.isInvalid(record)
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assertTrue(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Given an invalid proto with no name, assert invalid`() {
|
||||||
|
// GIVEN
|
||||||
|
val proto = NotificationProfile.Builder().apply {
|
||||||
|
id = UuidUtil.toByteArray(UUID.randomUUID()).toByteString()
|
||||||
|
name = ""
|
||||||
|
deletedAtTimestampMs = 0L
|
||||||
|
}.build()
|
||||||
|
val record = SignalNotificationProfileRecord(STORAGE_ID, proto)
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
val result = testSubject.isInvalid(record)
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assertTrue(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Given an invalid proto with a member that does not have a service id, assert invalid`() {
|
||||||
|
// GIVEN
|
||||||
|
val proto = NotificationProfile.Builder().apply {
|
||||||
|
id = UuidUtil.toByteArray(UUID.randomUUID()).toByteString()
|
||||||
|
name = "Profile"
|
||||||
|
allowedMembers = listOf(Recipient(contact = Recipient.Contact(serviceId = "bad")))
|
||||||
|
}.build()
|
||||||
|
val record = SignalNotificationProfileRecord(STORAGE_ID, proto)
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
val result = testSubject.isInvalid(record)
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
assertTrue(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package org.whispersystems.signalservice.api.storage
|
||||||
|
|
||||||
|
import org.whispersystems.signalservice.internal.storage.protos.NotificationProfile
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper around a [NotificationProfile] to pair it with a [StorageId].
|
||||||
|
*/
|
||||||
|
data class SignalNotificationProfileRecord(
|
||||||
|
override val id: StorageId,
|
||||||
|
override val proto: NotificationProfile
|
||||||
|
) : SignalRecord<NotificationProfile> {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun newBuilder(serializedUnknowns: ByteArray?): NotificationProfile.Builder {
|
||||||
|
return serializedUnknowns?.let { builderFromUnknowns(it) } ?: NotificationProfile.Builder()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun builderFromUnknowns(serializedUnknowns: ByteArray): NotificationProfile.Builder {
|
||||||
|
return try {
|
||||||
|
NotificationProfile.ADAPTER.decode(serializedUnknowns).newBuilder()
|
||||||
|
} catch (e: IOException) {
|
||||||
|
NotificationProfile.Builder()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,7 +10,7 @@ data class SignalStorageRecord(
|
|||||||
val proto: StorageRecord
|
val proto: StorageRecord
|
||||||
) {
|
) {
|
||||||
val isUnknown: Boolean
|
val isUnknown: Boolean
|
||||||
get() = proto.contact == null && proto.groupV1 == null && proto.groupV2 == null && proto.account == null && proto.storyDistributionList == null && proto.callLink == null && proto.chatFolder == null
|
get() = proto.contact == null && proto.groupV1 == null && proto.groupV2 == null && proto.account == null && proto.storyDistributionList == null && proto.callLink == null && proto.chatFolder == null && proto.notificationProfile == null
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
|
|||||||
@@ -42,6 +42,10 @@ public class StorageId {
|
|||||||
return new StorageId(ManifestRecord.Identifier.Type.CHAT_FOLDER.getValue(), Preconditions.checkNotNull(raw));
|
return new StorageId(ManifestRecord.Identifier.Type.CHAT_FOLDER.getValue(), Preconditions.checkNotNull(raw));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static StorageId forNotificationProfile(byte[] raw) {
|
||||||
|
return new StorageId(ManifestRecord.Identifier.Type.NOTIFICATION_PROFILE.getValue(), Preconditions.checkNotNull(raw));
|
||||||
|
}
|
||||||
|
|
||||||
public static StorageId forType(byte[] raw, int type) {
|
public static StorageId forType(byte[] raw, int type) {
|
||||||
return new StorageId(type, raw);
|
return new StorageId(type, raw);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import org.whispersystems.signalservice.internal.storage.protos.ChatFolderRecord
|
|||||||
import org.whispersystems.signalservice.internal.storage.protos.ContactRecord
|
import org.whispersystems.signalservice.internal.storage.protos.ContactRecord
|
||||||
import org.whispersystems.signalservice.internal.storage.protos.GroupV1Record
|
import org.whispersystems.signalservice.internal.storage.protos.GroupV1Record
|
||||||
import org.whispersystems.signalservice.internal.storage.protos.GroupV2Record
|
import org.whispersystems.signalservice.internal.storage.protos.GroupV2Record
|
||||||
|
import org.whispersystems.signalservice.internal.storage.protos.NotificationProfile
|
||||||
import org.whispersystems.signalservice.internal.storage.protos.StorageRecord
|
import org.whispersystems.signalservice.internal.storage.protos.StorageRecord
|
||||||
import org.whispersystems.signalservice.internal.storage.protos.StoryDistributionListRecord
|
import org.whispersystems.signalservice.internal.storage.protos.StoryDistributionListRecord
|
||||||
|
|
||||||
@@ -46,6 +47,10 @@ fun ChatFolderRecord.toSignalChatFolderRecord(storageId: StorageId): SignalChatF
|
|||||||
return SignalChatFolderRecord(storageId, this)
|
return SignalChatFolderRecord(storageId, this)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun NotificationProfile.toSignalNotificationProfileRecord(storageId: StorageId): SignalNotificationProfileRecord {
|
||||||
|
return SignalNotificationProfileRecord(storageId, this)
|
||||||
|
}
|
||||||
|
|
||||||
fun SignalContactRecord.toSignalStorageRecord(): SignalStorageRecord {
|
fun SignalContactRecord.toSignalStorageRecord(): SignalStorageRecord {
|
||||||
return SignalStorageRecord(id, StorageRecord(contact = this.proto))
|
return SignalStorageRecord(id, StorageRecord(contact = this.proto))
|
||||||
}
|
}
|
||||||
@@ -73,3 +78,7 @@ fun SignalCallLinkRecord.toSignalStorageRecord(): SignalStorageRecord {
|
|||||||
fun SignalChatFolderRecord.toSignalStorageRecord(): SignalStorageRecord {
|
fun SignalChatFolderRecord.toSignalStorageRecord(): SignalStorageRecord {
|
||||||
return SignalStorageRecord(id, StorageRecord(chatFolder = this.proto))
|
return SignalStorageRecord(id, StorageRecord(chatFolder = this.proto))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun SignalNotificationProfileRecord.toSignalStorageRecord(): SignalStorageRecord {
|
||||||
|
return SignalStorageRecord(id, StorageRecord(notificationProfile = this.proto))
|
||||||
|
}
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ message ManifestRecord {
|
|||||||
STORY_DISTRIBUTION_LIST = 5;
|
STORY_DISTRIBUTION_LIST = 5;
|
||||||
CALL_LINK = 7;
|
CALL_LINK = 7;
|
||||||
CHAT_FOLDER = 8;
|
CHAT_FOLDER = 8;
|
||||||
|
NOTIFICATION_PROFILE = 9;
|
||||||
}
|
}
|
||||||
|
|
||||||
bytes raw = 1;
|
bytes raw = 1;
|
||||||
@@ -74,6 +75,7 @@ message StorageRecord {
|
|||||||
StoryDistributionListRecord storyDistributionList = 5;
|
StoryDistributionListRecord storyDistributionList = 5;
|
||||||
CallLinkRecord callLink = 7;
|
CallLinkRecord callLink = 7;
|
||||||
ChatFolderRecord chatFolder = 8;
|
ChatFolderRecord chatFolder = 8;
|
||||||
|
NotificationProfile notificationProfile = 9;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,48 +227,70 @@ message AccountRecord {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bytes profileKey = 1;
|
message BackupTierHistory {
|
||||||
string givenName = 2;
|
// See zkgroup for integer particular values. Unset if backups are not enabled.
|
||||||
string familyName = 3;
|
optional uint64 backupTier = 1;
|
||||||
string avatarUrlPath = 4;
|
optional uint64 endedAtTimestamp = 2;
|
||||||
bool noteToSelfArchived = 5;
|
}
|
||||||
bool readReceipts = 6;
|
|
||||||
bool sealedSenderIndicators = 7;
|
message NotificationProfileManualOverride {
|
||||||
bool typingIndicators = 8;
|
message ManuallyEnabled {
|
||||||
reserved /* proxiedLinkPreviews */ 9;
|
bytes id = 1;
|
||||||
bool noteToSelfMarkedUnread = 10;
|
|
||||||
bool linkPreviews = 11;
|
// This will be unset if no timespan was chosen in the UI.
|
||||||
PhoneNumberSharingMode phoneNumberSharingMode = 12;
|
uint64 endAtTimestampMs = 3;
|
||||||
bool unlistedPhoneNumber = 13;
|
}
|
||||||
repeated PinnedConversation pinnedConversations = 14;
|
|
||||||
bool preferContactAvatars = 15;
|
oneof override {
|
||||||
Payments payments = 16;
|
uint64 disabledAtTimestampMs = 1;
|
||||||
uint32 universalExpireTimer = 17;
|
ManuallyEnabled enabled = 2;
|
||||||
bool primarySendsSms = 18;
|
}
|
||||||
string e164 = 19;
|
}
|
||||||
repeated string preferredReactionEmoji = 20;
|
|
||||||
bytes subscriberId = 21;
|
bytes profileKey = 1;
|
||||||
string subscriberCurrencyCode = 22;
|
string givenName = 2;
|
||||||
bool displayBadgesOnProfile = 23;
|
string familyName = 3;
|
||||||
bool subscriptionManuallyCancelled = 24;
|
string avatarUrlPath = 4;
|
||||||
bool keepMutedChatsArchived = 25;
|
bool noteToSelfArchived = 5;
|
||||||
bool hasSetMyStoriesPrivacy = 26;
|
bool readReceipts = 6;
|
||||||
bool hasViewedOnboardingStory = 27;
|
bool sealedSenderIndicators = 7;
|
||||||
reserved /* storiesDisabled */ 28;
|
bool typingIndicators = 8;
|
||||||
bool storiesDisabled = 29;
|
reserved /* proxiedLinkPreviews */ 9;
|
||||||
OptionalBool storyViewReceiptsEnabled = 30;
|
bool noteToSelfMarkedUnread = 10;
|
||||||
reserved /* hasReadOnboardingStory */ 31;
|
bool linkPreviews = 11;
|
||||||
bool hasSeenGroupStoryEducationSheet = 32;
|
PhoneNumberSharingMode phoneNumberSharingMode = 12;
|
||||||
string username = 33;
|
bool unlistedPhoneNumber = 13;
|
||||||
bool hasCompletedUsernameOnboarding = 34;
|
repeated PinnedConversation pinnedConversations = 14;
|
||||||
UsernameLink usernameLink = 35;
|
bool preferContactAvatars = 15;
|
||||||
reserved /* backupsSubscriberId */ 36;
|
Payments payments = 16;
|
||||||
reserved /* backupsSubscriberCurrencyCode */ 37;
|
uint32 universalExpireTimer = 17;
|
||||||
reserved /* backupsSubscriptionManuallyCancelled */ 38;
|
bool primarySendsSms = 18;
|
||||||
optional bool hasBackup = 39; // Set to true after backups are enabled and one is uploaded.
|
reserved /* e164 */ 19;
|
||||||
optional uint64 backupTier = 40; // See zkgroup for integer particular values
|
repeated string preferredReactionEmoji = 20;
|
||||||
IAPSubscriberData backupSubscriberData = 41;
|
bytes subscriberId = 21;
|
||||||
optional AvatarColor avatarColor = 42;
|
string subscriberCurrencyCode = 22;
|
||||||
|
bool displayBadgesOnProfile = 23;
|
||||||
|
bool subscriptionManuallyCancelled = 24;
|
||||||
|
bool keepMutedChatsArchived = 25;
|
||||||
|
bool hasSetMyStoriesPrivacy = 26;
|
||||||
|
bool hasViewedOnboardingStory = 27;
|
||||||
|
reserved /* storiesDisabled */ 28;
|
||||||
|
bool storiesDisabled = 29;
|
||||||
|
OptionalBool storyViewReceiptsEnabled = 30;
|
||||||
|
reserved /* hasReadOnboardingStory */ 31;
|
||||||
|
bool hasSeenGroupStoryEducationSheet = 32;
|
||||||
|
string username = 33;
|
||||||
|
bool hasCompletedUsernameOnboarding = 34;
|
||||||
|
UsernameLink usernameLink = 35;
|
||||||
|
reserved /* backupsSubscriberId */ 36;
|
||||||
|
reserved /* backupsSubscriberCurrencyCode */ 37;
|
||||||
|
reserved /* backupsSubscriptionManuallyCancelled */ 38;
|
||||||
|
optional bool hasBackup = 39; // Set to true after backups are enabled and one is uploaded.
|
||||||
|
optional uint64 backupTier = 40; // See zkgroup for integer particular values. Unset if backups are not enabled.
|
||||||
|
IAPSubscriberData backupSubscriberData = 41;
|
||||||
|
optional AvatarColor avatarColor = 42;
|
||||||
|
BackupTierHistory backupTierHistory = 43;
|
||||||
|
NotificationProfileManualOverride notificationProfileManualOverride = 44;
|
||||||
}
|
}
|
||||||
|
|
||||||
message StoryDistributionListRecord {
|
message StoryDistributionListRecord {
|
||||||
@@ -284,20 +308,20 @@ message CallLinkRecord {
|
|||||||
uint64 deletedAtTimestampMs = 3;
|
uint64 deletedAtTimestampMs = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
message ChatFolderRecord {
|
message Recipient {
|
||||||
message Recipient {
|
message Contact {
|
||||||
message Contact {
|
string serviceId = 1;
|
||||||
string serviceId = 1;
|
string e164 = 2;
|
||||||
string e164 = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
oneof identifier {
|
|
||||||
Contact contact = 1;
|
|
||||||
bytes legacyGroupId = 2;
|
|
||||||
bytes groupMasterKey = 3;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
oneof identifier {
|
||||||
|
Contact contact = 1;
|
||||||
|
bytes legacyGroupId = 2;
|
||||||
|
bytes groupMasterKey = 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message ChatFolderRecord {
|
||||||
// Represents the default "All chats" folder record vs all other custom folders
|
// Represents the default "All chats" folder record vs all other custom folders
|
||||||
enum FolderType {
|
enum FolderType {
|
||||||
UNKNOWN = 0;
|
UNKNOWN = 0;
|
||||||
@@ -317,3 +341,30 @@ message ChatFolderRecord {
|
|||||||
repeated Recipient excludedRecipients = 10;
|
repeated Recipient excludedRecipients = 10;
|
||||||
uint64 deletedAtTimestampMs = 11; // When non-zero, `position` should be set to -1 and includedRecipients should be empty
|
uint64 deletedAtTimestampMs = 11; // When non-zero, `position` should be set to -1 and includedRecipients should be empty
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message NotificationProfile {
|
||||||
|
enum DayOfWeek {
|
||||||
|
UNKNOWN = 0; // Interpret as "Monday"
|
||||||
|
MONDAY = 1;
|
||||||
|
TUESDAY = 2;
|
||||||
|
WEDNESDAY = 3;
|
||||||
|
THURSDAY = 4;
|
||||||
|
FRIDAY = 5;
|
||||||
|
SATURDAY = 6;
|
||||||
|
SUNDAY = 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes id = 1;
|
||||||
|
string name = 2;
|
||||||
|
optional string emoji = 3;
|
||||||
|
fixed32 color = 4; // 0xAARRGGBB
|
||||||
|
uint64 createdAtMs = 5;
|
||||||
|
bool allowAllCalls = 6;
|
||||||
|
bool allowAllMentions = 7;
|
||||||
|
repeated Recipient allowedMembers = 8;
|
||||||
|
bool scheduleEnabled = 9;
|
||||||
|
uint32 scheduleStartTime = 10; // 24-hour clock int, 0000-2359 (e.g., 15, 900, 1130, 2345)
|
||||||
|
uint32 scheduleEndTime = 11; // 24-hour clock int, 0000-2359 (e.g., 15, 900, 1130, 2345)
|
||||||
|
repeated DayOfWeek scheduleDaysEnabled = 12;
|
||||||
|
uint64 deletedAtTimestampMs = 13;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user