Add support for backing up wallpapers.

This commit is contained in:
Greyson Parrelli
2024-09-20 12:24:57 -04:00
committed by GitHub
parent e14078d2ec
commit a7bdfb6d76
30 changed files with 907 additions and 410 deletions

View File

@@ -0,0 +1,45 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.attachments
import org.thoughtcrime.securesms.database.AttachmentTable
import org.thoughtcrime.securesms.database.AttachmentTable.TransformProperties
import org.thoughtcrime.securesms.util.MediaUtil
/**
* A basically-empty [Attachment] that is solely used for inserting an attachment into the [AttachmentTable].
*/
class WallpaperAttachment() : Attachment(
contentType = MediaUtil.IMAGE_WEBP,
transferState = AttachmentTable.TRANSFER_PROGRESS_DONE,
size = 0,
fileName = null,
cdn = Cdn.CDN_0,
remoteLocation = null,
remoteKey = null,
remoteIv = null,
remoteDigest = null,
incrementalDigest = null,
fastPreflightId = null,
voiceNote = false,
borderless = false,
videoGif = false,
width = 0,
height = 0,
incrementalMacChunkSize = 0,
quote = false,
uploadTimestamp = 0,
caption = null,
stickerLocator = null,
blurHash = null,
audioHash = null,
transformProperties = TransformProperties.empty(),
uuid = null
) {
override val uri = null
override val publicUri = null
override val thumbnailUri = null
}

View File

@@ -6,8 +6,14 @@
package org.thoughtcrime.securesms.backup.v2.database
import org.signal.core.util.deleteAll
import org.thoughtcrime.securesms.attachments.Attachment
import org.thoughtcrime.securesms.attachments.AttachmentId
import org.thoughtcrime.securesms.database.AttachmentTable
fun AttachmentTable.clearAllDataForBackupRestore() {
writableDatabase.deleteAll(AttachmentTable.TABLE_NAME)
}
fun AttachmentTable.restoreWallpaperAttachment(attachment: Attachment): AttachmentId? {
return insertAttachmentsForMessage(AttachmentTable.WALLPAPER_MESSAGE_ID, listOf(attachment), emptyList()).values.firstOrNull()
}

View File

@@ -7,7 +7,6 @@ package org.thoughtcrime.securesms.backup.v2.database
import android.content.ContentValues
import androidx.core.content.contentValuesOf
import okio.ByteString
import org.signal.core.util.Base64
import org.signal.core.util.Hex
import org.signal.core.util.SqlUtil
@@ -17,17 +16,13 @@ import org.signal.core.util.orNull
import org.signal.core.util.requireLong
import org.signal.core.util.toInt
import org.signal.core.util.update
import org.thoughtcrime.securesms.attachments.ArchivedAttachment
import org.thoughtcrime.securesms.attachments.Attachment
import org.thoughtcrime.securesms.attachments.Cdn
import org.thoughtcrime.securesms.attachments.PointerAttachment
import org.thoughtcrime.securesms.attachments.TombstoneAttachment
import org.thoughtcrime.securesms.backup.v2.ImportState
import org.thoughtcrime.securesms.backup.v2.proto.BodyRange
import org.thoughtcrime.securesms.backup.v2.proto.ChatItem
import org.thoughtcrime.securesms.backup.v2.proto.ChatUpdateMessage
import org.thoughtcrime.securesms.backup.v2.proto.ContactAttachment
import org.thoughtcrime.securesms.backup.v2.proto.FilePointer
import org.thoughtcrime.securesms.backup.v2.proto.GroupCall
import org.thoughtcrime.securesms.backup.v2.proto.IndividualCall
import org.thoughtcrime.securesms.backup.v2.proto.LinkPreview
@@ -39,8 +34,8 @@ import org.thoughtcrime.securesms.backup.v2.proto.SendStatus
import org.thoughtcrime.securesms.backup.v2.proto.SimpleChatUpdate
import org.thoughtcrime.securesms.backup.v2.proto.StandardMessage
import org.thoughtcrime.securesms.backup.v2.proto.Sticker
import org.thoughtcrime.securesms.backup.v2.util.toLocalAttachment
import org.thoughtcrime.securesms.contactshare.Contact
import org.thoughtcrime.securesms.database.AttachmentTable
import org.thoughtcrime.securesms.database.CallTable
import org.thoughtcrime.securesms.database.GroupReceiptTable
import org.thoughtcrime.securesms.database.MessageTable
@@ -74,9 +69,6 @@ import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.stickers.StickerLocator
import org.thoughtcrime.securesms.util.JsonUtils
import org.whispersystems.signalservice.api.backup.MediaName
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId
import org.whispersystems.signalservice.api.payments.Money
import org.whispersystems.signalservice.api.push.ServiceId
import org.whispersystems.signalservice.api.util.UuidUtil
@@ -342,7 +334,7 @@ class ChatItemImportInserter(
address.country
)
},
Contact.Avatar(null, backupContact.avatar.toLocalAttachment(), true)
Contact.Avatar(null, backupContact.avatar.toLocalAttachment(importState = importState, voiceNote = false, borderless = false, gif = false, wasDownloaded = true), true)
)
}
@@ -917,29 +909,6 @@ class ChatItemImportInserter(
?: false
}
private fun LinkPreview.toLocalLinkPreview(): org.thoughtcrime.securesms.linkpreview.LinkPreview {
return org.thoughtcrime.securesms.linkpreview.LinkPreview(
this.url,
this.title ?: "",
this.description ?: "",
this.date ?: 0,
Optional.ofNullable(this.image?.toLocalAttachment())
)
}
private fun MessageAttachment.toLocalAttachment(contentType: String? = this.pointer?.contentType, fileName: String? = this.pointer?.fileName): Attachment? {
return this.pointer?.toLocalAttachment(
voiceNote = this.flag == MessageAttachment.Flag.VOICE_MESSAGE,
borderless = this.flag == MessageAttachment.Flag.BORDERLESS,
gif = this.flag == MessageAttachment.Flag.GIF,
wasDownloaded = this.wasDownloaded,
stickerLocator = null,
contentType = contentType,
fileName = fileName,
uuid = this.clientUuid
)
}
private fun Quote.QuotedAttachment.toLocalAttachment(): Attachment? {
val thumbnail = this.thumbnail?.toLocalAttachment(this.contentType, this.fileName)
@@ -962,6 +931,11 @@ class ChatItemImportInserter(
if (this == null) return null
return data_.toLocalAttachment(
importState = importState,
voiceNote = false,
gif = false,
borderless = false,
wasDownloaded = true,
stickerLocator = StickerLocator(
packId = Hex.toStringCondensed(packId.toByteArray()),
packKey = Hex.toStringCondensed(packKey.toByteArray()),
@@ -971,90 +945,38 @@ class ChatItemImportInserter(
)
}
private fun FilePointer?.toLocalAttachment(
borderless: Boolean = false,
gif: Boolean = false,
voiceNote: Boolean = false,
wasDownloaded: Boolean = true,
stickerLocator: StickerLocator? = null,
contentType: String? = this?.contentType,
fileName: String? = this?.fileName,
uuid: ByteString? = null
): Attachment? {
return if (this == null) {
null
} else if (this.attachmentLocator != null) {
val signalAttachmentPointer = SignalServiceAttachmentPointer(
cdnNumber = this.attachmentLocator.cdnNumber,
remoteId = SignalServiceAttachmentRemoteId.from(this.attachmentLocator.cdnKey),
contentType = contentType,
key = this.attachmentLocator.key.toByteArray(),
size = Optional.ofNullable(this.attachmentLocator.size),
preview = Optional.empty(),
width = this.width ?: 0,
height = this.height ?: 0,
digest = Optional.ofNullable(this.attachmentLocator.digest.toByteArray()),
incrementalDigest = Optional.ofNullable(this.incrementalMac?.toByteArray()),
incrementalMacChunkSize = this.incrementalMacChunkSize ?: 0,
fileName = Optional.ofNullable(fileName),
voiceNote = voiceNote,
isBorderless = borderless,
isGif = gif,
caption = Optional.ofNullable(this.caption),
blurHash = Optional.ofNullable(this.blurHash),
uploadTimestamp = this.attachmentLocator.uploadTimestamp,
uuid = UuidUtil.fromByteStringOrNull(uuid)
)
PointerAttachment.forPointer(
pointer = Optional.of(signalAttachmentPointer),
stickerLocator = stickerLocator,
transferState = if (wasDownloaded) AttachmentTable.TRANSFER_NEEDS_RESTORE else AttachmentTable.TRANSFER_PROGRESS_PENDING
).orNull()
} else if (this.invalidAttachmentLocator != null) {
TombstoneAttachment(
contentType = contentType,
incrementalMac = this.incrementalMac?.toByteArray(),
incrementalMacChunkSize = this.incrementalMacChunkSize,
width = this.width,
height = this.height,
caption = this.caption,
blurHash = this.blurHash,
voiceNote = voiceNote,
borderless = borderless,
gif = gif,
quote = false,
uuid = UuidUtil.fromByteStringOrNull(uuid)
)
} else if (this.backupLocator != null) {
ArchivedAttachment(
contentType = contentType,
size = this.backupLocator.size.toLong(),
cdn = this.backupLocator.transitCdnNumber ?: Cdn.CDN_0.cdnNumber,
key = this.backupLocator.key.toByteArray(),
iv = null,
cdnKey = this.backupLocator.transitCdnKey,
archiveCdn = this.backupLocator.cdnNumber,
archiveMediaName = this.backupLocator.mediaName,
archiveMediaId = importState.backupKey.deriveMediaId(MediaName(this.backupLocator.mediaName)).encode(),
archiveThumbnailMediaId = importState.backupKey.deriveMediaId(MediaName.forThumbnailFromMediaName(this.backupLocator.mediaName)).encode(),
digest = this.backupLocator.digest.toByteArray(),
incrementalMac = this.incrementalMac?.toByteArray(),
incrementalMacChunkSize = this.incrementalMacChunkSize,
width = this.width,
height = this.height,
caption = this.caption,
blurHash = this.blurHash,
voiceNote = voiceNote,
borderless = borderless,
gif = gif,
quote = false,
stickerLocator = stickerLocator,
uuid = UuidUtil.fromByteStringOrNull(uuid),
fileName = fileName
)
} else {
null
}
private fun LinkPreview.toLocalLinkPreview(): org.thoughtcrime.securesms.linkpreview.LinkPreview {
return org.thoughtcrime.securesms.linkpreview.LinkPreview(
this.url,
this.title ?: "",
this.description ?: "",
this.date ?: 0,
Optional.ofNullable(this.image?.toLocalAttachment(importState = importState, voiceNote = false, borderless = false, gif = false, wasDownloaded = true))
)
}
private fun MessageAttachment.toLocalAttachment(): Attachment? {
return pointer?.toLocalAttachment(
importState = importState,
voiceNote = flag == MessageAttachment.Flag.VOICE_MESSAGE,
gif = flag == MessageAttachment.Flag.GIF,
borderless = flag == MessageAttachment.Flag.BORDERLESS,
wasDownloaded = wasDownloaded,
uuid = clientUuid
)
}
private fun MessageAttachment.toLocalAttachment(contentType: String?, fileName: String?): Attachment? {
return pointer?.toLocalAttachment(
importState = importState,
voiceNote = flag == MessageAttachment.Flag.VOICE_MESSAGE,
gif = flag == MessageAttachment.Flag.GIF,
borderless = flag == MessageAttachment.Flag.BORDERLESS,
wasDownloaded = wasDownloaded,
contentType = contentType,
fileName = fileName,
uuid = clientUuid
)
}
private fun ContactAttachment.Name?.toLocal(): Contact.Name {

View File

@@ -7,7 +7,6 @@ package org.thoughtcrime.securesms.backup.v2.database
import android.database.Cursor
import androidx.core.content.contentValuesOf
import com.google.protobuf.InvalidProtocolBufferException
import org.signal.core.util.SqlUtil
import org.signal.core.util.insertInto
import org.signal.core.util.logging.Log
@@ -16,15 +15,26 @@ import org.signal.core.util.requireBoolean
import org.signal.core.util.requireInt
import org.signal.core.util.requireLong
import org.signal.core.util.toInt
import org.thoughtcrime.securesms.attachments.AttachmentId
import org.thoughtcrime.securesms.backup.v2.ImportState
import org.thoughtcrime.securesms.backup.v2.proto.Chat
import org.thoughtcrime.securesms.backup.v2.util.BackupConverters
import org.thoughtcrime.securesms.backup.v2.proto.ChatStyle
import org.thoughtcrime.securesms.backup.v2.util.ChatStyleConverter
import org.thoughtcrime.securesms.backup.v2.util.toLocal
import org.thoughtcrime.securesms.backup.v2.util.toLocalAttachment
import org.thoughtcrime.securesms.conversation.colors.ChatColors
import org.thoughtcrime.securesms.database.RecipientTable
import org.thoughtcrime.securesms.database.SQLiteDatabase
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.ThreadTable
import org.thoughtcrime.securesms.database.model.databaseprotos.ChatColor
import org.thoughtcrime.securesms.database.model.databaseprotos.Wallpaper
import org.thoughtcrime.securesms.mms.PartAuthority
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.util.decodeOrNull
import org.thoughtcrime.securesms.wallpaper.ChatWallpaper
import org.thoughtcrime.securesms.wallpaper.ChatWallpaperFactory
import org.thoughtcrime.securesms.wallpaper.UriChatWallpaper
import java.io.Closeable
private val TAG = Log.tag(ThreadTable::class.java)
@@ -43,14 +53,15 @@ fun ThreadTable.getThreadsForBackup(): ChatExportIterator {
${RecipientTable.TABLE_NAME}.${RecipientTable.MUTE_UNTIL},
${RecipientTable.TABLE_NAME}.${RecipientTable.MENTION_SETTING},
${RecipientTable.TABLE_NAME}.${RecipientTable.CHAT_COLORS},
${RecipientTable.TABLE_NAME}.${RecipientTable.CUSTOM_CHAT_COLORS_ID}
${RecipientTable.TABLE_NAME}.${RecipientTable.CUSTOM_CHAT_COLORS_ID},
${RecipientTable.TABLE_NAME}.${RecipientTable.WALLPAPER}
FROM ${ThreadTable.TABLE_NAME}
LEFT OUTER JOIN ${RecipientTable.TABLE_NAME} ON ${ThreadTable.TABLE_NAME}.${ThreadTable.RECIPIENT_ID} = ${RecipientTable.TABLE_NAME}.${RecipientTable.ID}
WHERE ${ThreadTable.ACTIVE} = 1
"""
val cursor = readableDatabase.query(query)
return ChatExportIterator(cursor)
return ChatExportIterator(cursor, readableDatabase)
}
fun ThreadTable.clearAllDataForBackupRestore() {
@@ -59,10 +70,29 @@ fun ThreadTable.clearAllDataForBackupRestore() {
clearCache()
}
fun ThreadTable.restoreFromBackup(chat: Chat, recipientId: RecipientId, importState: ImportState): Long? {
fun ThreadTable.restoreFromBackup(chat: Chat, recipientId: RecipientId, importState: ImportState): Long {
val chatColor = chat.style?.toLocal(importState)
val chatColorWithId = if (chatColor != null && chatColor.id is ChatColors.Id.NotSet) {
val savedColors = SignalDatabase.chatColors.getSavedChatColors()
val match = savedColors.find { it.matchesWithoutId(chatColor) }
match ?: SignalDatabase.chatColors.saveChatColors(chatColor)
} else {
chatColor
}
// TODO [backup] Wallpaper
val wallpaperAttachmentId: AttachmentId? = chat.style?.wallpaperPhoto?.let { filePointer ->
filePointer.toLocalAttachment(importState)?.let {
SignalDatabase.attachments.restoreWallpaperAttachment(it)
}
}
val chatWallpaper = chat.style?.parseChatWallpaper(wallpaperAttachmentId)?.let {
if (chat.style.dimWallpaperInDarkMode) {
ChatWallpaperFactory.updateWithDimming(it, ChatWallpaper.FIXED_DIM_LEVEL_FOR_DARK_THEME)
} else {
it
}
}
val threadId = writableDatabase
.insertInto(ThreadTable.TABLE_NAME)
@@ -84,7 +114,9 @@ fun ThreadTable.restoreFromBackup(chat: Chat, recipientId: RecipientId, importSt
RecipientTable.MESSAGE_EXPIRATION_TIME to chat.expirationTimerMs,
RecipientTable.MESSAGE_EXPIRATION_TIME_VERSION to chat.expireTimerVersion,
RecipientTable.CHAT_COLORS to chatColor?.serialize()?.encode(),
RecipientTable.CUSTOM_CHAT_COLORS_ID to (chatColor?.id ?: ChatColors.Id.NotSet).longValue
RecipientTable.CUSTOM_CHAT_COLORS_ID to (chatColor?.id ?: ChatColors.Id.NotSet).longValue,
RecipientTable.WALLPAPER_URI to if (chatWallpaper is UriChatWallpaper) chatWallpaper.uri.toString() else null,
RecipientTable.WALLPAPER to chatWallpaper?.serialize()?.encode()
),
"${RecipientTable.ID} = ?",
SqlUtil.buildArgs(recipientId.toLong())
@@ -93,7 +125,7 @@ fun ThreadTable.restoreFromBackup(chat: Chat, recipientId: RecipientId, importSt
return threadId
}
class ChatExportIterator(private val cursor: Cursor) : Iterator<Chat>, Closeable {
class ChatExportIterator(private val cursor: Cursor, private val readableDatabase: SQLiteDatabase) : Iterator<Chat>, Closeable {
override fun hasNext(): Boolean {
return cursor.count > 0 && !cursor.isLast
}
@@ -103,14 +135,15 @@ class ChatExportIterator(private val cursor: Cursor) : Iterator<Chat>, Closeable
throw NoSuchElementException()
}
val serializedChatColors = cursor.requireBlob(RecipientTable.CHAT_COLORS)
val chatColorId = ChatColors.Id.forLongValue(cursor.requireLong(RecipientTable.CUSTOM_CHAT_COLORS_ID))
val chatColors: ChatColors? = serializedChatColors?.let { serialized ->
try {
ChatColors.forChatColor(chatColorId, ChatColor.ADAPTER.decode(serialized))
} catch (e: InvalidProtocolBufferException) {
null
}
val customChatColorsId = ChatColors.Id.forLongValue(cursor.requireLong(RecipientTable.CUSTOM_CHAT_COLORS_ID))
val chatColors: ChatColors? = cursor.requireBlob(RecipientTable.CHAT_COLORS)?.let { serializedChatColors ->
val chatColor = ChatColor.ADAPTER.decodeOrNull(serializedChatColors)
chatColor?.let { ChatColors.forChatColor(customChatColorsId, it) }
}
val chatWallpaper: Wallpaper? = cursor.requireBlob(RecipientTable.WALLPAPER)?.let { serializedWallpaper ->
Wallpaper.ADAPTER.decodeOrNull(serializedWallpaper)
}
return Chat(
@@ -123,7 +156,12 @@ class ChatExportIterator(private val cursor: Cursor) : Iterator<Chat>, Closeable
muteUntilMs = cursor.requireLong(RecipientTable.MUTE_UNTIL),
markedUnread = ThreadTable.ReadStatus.deserialize(cursor.requireInt(ThreadTable.READ)) == ThreadTable.ReadStatus.FORCED_UNREAD,
dontNotifyForMentionsIfMuted = RecipientTable.MentionSetting.DO_NOT_NOTIFY.id == cursor.requireInt(RecipientTable.MENTION_SETTING),
style = BackupConverters.constructRemoteChatStyle(chatColors, chatColorId)
style = ChatStyleConverter.constructRemoteChatStyle(
readableDatabase = readableDatabase,
chatColors = chatColors,
chatColorId = customChatColorsId,
chatWallpaper = chatWallpaper
)
)
}
@@ -131,3 +169,15 @@ class ChatExportIterator(private val cursor: Cursor) : Iterator<Chat>, Closeable
cursor.close()
}
}
private fun ChatStyle.parseChatWallpaper(wallpaperAttachmentId: AttachmentId?): ChatWallpaper? {
if (this.wallpaperPreset != null) {
return this.wallpaperPreset.toLocal()
}
if (wallpaperAttachmentId != null) {
return UriChatWallpaper(PartAuthority.getAttachmentDataUri(wallpaperAttachmentId), 0f)
}
return null
}

View File

@@ -14,7 +14,7 @@ import org.thoughtcrime.securesms.backup.v2.proto.AccountData
import org.thoughtcrime.securesms.backup.v2.proto.ChatStyle
import org.thoughtcrime.securesms.backup.v2.proto.Frame
import org.thoughtcrime.securesms.backup.v2.stream.BackupFrameEmitter
import org.thoughtcrime.securesms.backup.v2.util.BackupConverters
import org.thoughtcrime.securesms.backup.v2.util.ChatStyleConverter
import org.thoughtcrime.securesms.backup.v2.util.toLocal
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.UsernameQrCodeColorScheme
@@ -50,6 +50,7 @@ object AccountDataProcessor {
val donationSubscriber = db.inAppPaymentSubscriberTable.getByCurrencyCode(donationCurrency.currencyCode, InAppPaymentSubscriberRecord.Type.DONATION)
val chatColors = SignalStore.chatColors.chatColors
val chatWallpaper = SignalStore.wallpaper.currentRawWallpaper
emitter.emit(
Frame(
@@ -87,12 +88,12 @@ object AccountDataProcessor {
hasSeenGroupStoryEducationSheet = signalStore.storyValues.userHasSeenGroupStoryEducationSheet,
hasCompletedUsernameOnboarding = signalStore.uiHintValues.hasCompletedUsernameOnboarding(),
customChatColors = db.chatColorsTable.getSavedChatColors().toRemoteChatColors(),
defaultChatStyle = BackupConverters.constructRemoteChatStyle(chatColors, chatColors?.id ?: ChatColors.Id.NotSet)?.also {
it.newBuilder().apply {
// TODO [backup] We should do this elsewhere once we handle wallpaper better
dimWallpaperInDarkMode = (SignalStore.wallpaper.wallpaper?.dimLevelForDarkTheme ?: 0f) > 0f
}.build()
}
defaultChatStyle = ChatStyleConverter.constructRemoteChatStyle(
readableDatabase = db.signalReadableDatabase,
chatColors = chatColors,
chatColorId = chatColors?.id ?: ChatColors.Id.NotSet,
chatWallpaper = chatWallpaper
)
),
donationSubscriberData = donationSubscriber?.toSubscriberData(signalStore.inAppPaymentValues.isDonationSubscriptionManuallyCancelled())
)

View File

@@ -39,7 +39,7 @@ object ChatBackupProcessor {
return
}
SignalDatabase.threads.restoreFromBackup(chat, recipientId, importState)?.let { threadId ->
SignalDatabase.threads.restoreFromBackup(chat, recipientId, importState).let { threadId ->
importState.chatIdToLocalRecipientId[chat.id] = recipientId
importState.chatIdToLocalThreadId[chat.id] = threadId
importState.chatIdToBackupRecipientId[chat.id] = chat.recipientId

View File

@@ -0,0 +1,112 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2.util
import okio.ByteString
import org.signal.core.util.orNull
import org.thoughtcrime.securesms.attachments.ArchivedAttachment
import org.thoughtcrime.securesms.attachments.Attachment
import org.thoughtcrime.securesms.attachments.Cdn
import org.thoughtcrime.securesms.attachments.PointerAttachment
import org.thoughtcrime.securesms.attachments.TombstoneAttachment
import org.thoughtcrime.securesms.backup.v2.ImportState
import org.thoughtcrime.securesms.backup.v2.proto.FilePointer
import org.thoughtcrime.securesms.database.AttachmentTable
import org.thoughtcrime.securesms.stickers.StickerLocator
import org.whispersystems.signalservice.api.backup.MediaName
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId
import org.whispersystems.signalservice.api.util.UuidUtil
import java.util.Optional
/**
* Converts a [FilePointer] to a local [Attachment] object for inserting into the database.
*/
fun FilePointer?.toLocalAttachment(
importState: ImportState,
voiceNote: Boolean = false,
borderless: Boolean = false,
gif: Boolean = false,
wasDownloaded: Boolean = false,
stickerLocator: StickerLocator? = null,
contentType: String? = this?.contentType,
fileName: String? = this?.fileName,
uuid: ByteString? = null
): Attachment? {
if (this == null) return null
if (this.attachmentLocator != null) {
val signalAttachmentPointer = SignalServiceAttachmentPointer(
cdnNumber = this.attachmentLocator.cdnNumber,
remoteId = SignalServiceAttachmentRemoteId.from(attachmentLocator.cdnKey),
contentType = contentType,
key = this.attachmentLocator.key.toByteArray(),
size = Optional.ofNullable(attachmentLocator.size),
preview = Optional.empty(),
width = this.width ?: 0,
height = this.height ?: 0,
digest = Optional.ofNullable(this.attachmentLocator.digest.toByteArray()),
incrementalDigest = Optional.ofNullable(this.incrementalMac?.toByteArray()),
incrementalMacChunkSize = this.incrementalMacChunkSize ?: 0,
fileName = Optional.ofNullable(fileName),
voiceNote = voiceNote,
isBorderless = borderless,
isGif = gif,
caption = Optional.ofNullable(this.caption),
blurHash = Optional.ofNullable(this.blurHash),
uploadTimestamp = this.attachmentLocator.uploadTimestamp,
uuid = UuidUtil.fromByteStringOrNull(uuid)
)
return PointerAttachment.forPointer(
pointer = Optional.of(signalAttachmentPointer),
stickerLocator = stickerLocator,
transferState = if (wasDownloaded) AttachmentTable.TRANSFER_NEEDS_RESTORE else AttachmentTable.TRANSFER_PROGRESS_PENDING
).orNull()
} else if (this.invalidAttachmentLocator != null) {
return TombstoneAttachment(
contentType = contentType,
incrementalMac = this.incrementalMac?.toByteArray(),
incrementalMacChunkSize = this.incrementalMacChunkSize,
width = this.width,
height = this.height,
caption = this.caption,
blurHash = this.blurHash,
voiceNote = voiceNote,
borderless = borderless,
gif = gif,
quote = false,
uuid = UuidUtil.fromByteStringOrNull(uuid)
)
} else if (this.backupLocator != null) {
return ArchivedAttachment(
contentType = contentType,
size = this.backupLocator.size.toLong(),
cdn = this.backupLocator.transitCdnNumber ?: Cdn.CDN_0.cdnNumber,
key = this.backupLocator.key.toByteArray(),
iv = null,
cdnKey = this.backupLocator.transitCdnKey,
archiveCdn = this.backupLocator.cdnNumber,
archiveMediaName = this.backupLocator.mediaName,
archiveMediaId = importState.backupKey.deriveMediaId(MediaName(this.backupLocator.mediaName)).encode(),
archiveThumbnailMediaId = importState.backupKey.deriveMediaId(MediaName.forThumbnailFromMediaName(this.backupLocator.mediaName)).encode(),
digest = this.backupLocator.digest.toByteArray(),
incrementalMac = this.incrementalMac?.toByteArray(),
incrementalMacChunkSize = this.incrementalMacChunkSize,
width = this.width,
height = this.height,
caption = this.caption,
blurHash = this.blurHash,
voiceNote = voiceNote,
borderless = borderless,
gif = gif,
quote = false,
stickerLocator = stickerLocator,
uuid = UuidUtil.fromByteStringOrNull(uuid),
fileName = fileName
)
}
return null
}

View File

@@ -0,0 +1,279 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2.util
import android.database.Cursor
import okio.ByteString
import okio.ByteString.Companion.toByteString
import org.signal.core.util.Base64
import org.signal.core.util.readToSingleObject
import org.signal.core.util.requireBlob
import org.signal.core.util.requireInt
import org.signal.core.util.requireLong
import org.signal.core.util.requireString
import org.signal.core.util.select
import org.thoughtcrime.securesms.attachments.AttachmentId
import org.thoughtcrime.securesms.backup.v2.ImportState
import org.thoughtcrime.securesms.backup.v2.proto.ChatStyle
import org.thoughtcrime.securesms.backup.v2.proto.FilePointer
import org.thoughtcrime.securesms.conversation.colors.ChatColors
import org.thoughtcrime.securesms.conversation.colors.ChatColorsPalette
import org.thoughtcrime.securesms.database.AttachmentTable
import org.thoughtcrime.securesms.database.SQLiteDatabase
import org.thoughtcrime.securesms.database.model.databaseprotos.Wallpaper
import org.thoughtcrime.securesms.mms.PartUriParser
import org.thoughtcrime.securesms.util.UriUtil
import org.thoughtcrime.securesms.wallpaper.ChatWallpaper
import org.thoughtcrime.securesms.wallpaper.GradientChatWallpaper
import org.thoughtcrime.securesms.wallpaper.SingleColorChatWallpaper
/**
* Contains a collection of methods to chat styles to and from their archive format.
* These are in a file of their own just because they're rather long (with all of the various constants to map between) and used in multiple places.
*/
object ChatStyleConverter {
fun constructRemoteChatStyle(
readableDatabase: SQLiteDatabase,
chatColors: ChatColors?,
chatColorId: ChatColors.Id,
chatWallpaper: Wallpaper?
): ChatStyle? {
if (chatColors == null && chatWallpaper == null) {
return null
}
val chatStyleBuilder = ChatStyle.Builder()
if (chatColors != null) {
when (chatColorId) {
ChatColors.Id.NotSet -> {}
ChatColors.Id.Auto -> {
chatStyleBuilder.autoBubbleColor = ChatStyle.AutomaticBubbleColor()
}
ChatColors.Id.BuiltIn -> {
chatStyleBuilder.bubbleColorPreset = chatColors.toRemote()
}
is ChatColors.Id.Custom -> {
chatStyleBuilder.customColorId = chatColorId.longValue
}
}
}
if (chatWallpaper != null) {
when {
chatWallpaper.singleColor != null -> {
chatStyleBuilder.wallpaperPreset = chatWallpaper.singleColor.color.toRemoteWallpaperPreset()
}
chatWallpaper.linearGradient != null -> {
chatStyleBuilder.wallpaperPreset = chatWallpaper.linearGradient.toRemoteWallpaperPreset()
}
chatWallpaper.file_ != null -> {
chatStyleBuilder.wallpaperPhoto = chatWallpaper.file_.toFilePointer(readableDatabase)
}
}
chatStyleBuilder.dimWallpaperInDarkMode = chatWallpaper.dimLevelInDarkTheme > 0
}
return chatStyleBuilder.build()
}
}
fun ChatStyle.toLocal(importState: ImportState): ChatColors? {
if (this.bubbleColorPreset != null) {
return when (this.bubbleColorPreset) {
// Solids
ChatStyle.BubbleColorPreset.SOLID_CRIMSON -> ChatColorsPalette.Bubbles.CRIMSON
ChatStyle.BubbleColorPreset.SOLID_VERMILION -> ChatColorsPalette.Bubbles.VERMILION
ChatStyle.BubbleColorPreset.SOLID_BURLAP -> ChatColorsPalette.Bubbles.BURLAP
ChatStyle.BubbleColorPreset.SOLID_FOREST -> ChatColorsPalette.Bubbles.FOREST
ChatStyle.BubbleColorPreset.SOLID_WINTERGREEN -> ChatColorsPalette.Bubbles.WINTERGREEN
ChatStyle.BubbleColorPreset.SOLID_TEAL -> ChatColorsPalette.Bubbles.TEAL
ChatStyle.BubbleColorPreset.SOLID_BLUE -> ChatColorsPalette.Bubbles.BLUE
ChatStyle.BubbleColorPreset.SOLID_INDIGO -> ChatColorsPalette.Bubbles.INDIGO
ChatStyle.BubbleColorPreset.SOLID_VIOLET -> ChatColorsPalette.Bubbles.VIOLET
ChatStyle.BubbleColorPreset.SOLID_PLUM -> ChatColorsPalette.Bubbles.PLUM
ChatStyle.BubbleColorPreset.SOLID_TAUPE -> ChatColorsPalette.Bubbles.TAUPE
ChatStyle.BubbleColorPreset.SOLID_STEEL -> ChatColorsPalette.Bubbles.STEEL
// Gradients
ChatStyle.BubbleColorPreset.GRADIENT_EMBER -> ChatColorsPalette.Bubbles.EMBER
ChatStyle.BubbleColorPreset.GRADIENT_MIDNIGHT -> ChatColorsPalette.Bubbles.MIDNIGHT
ChatStyle.BubbleColorPreset.GRADIENT_INFRARED -> ChatColorsPalette.Bubbles.INFRARED
ChatStyle.BubbleColorPreset.GRADIENT_LAGOON -> ChatColorsPalette.Bubbles.LAGOON
ChatStyle.BubbleColorPreset.GRADIENT_FLUORESCENT -> ChatColorsPalette.Bubbles.FLUORESCENT
ChatStyle.BubbleColorPreset.GRADIENT_BASIL -> ChatColorsPalette.Bubbles.BASIL
ChatStyle.BubbleColorPreset.GRADIENT_SUBLIME -> ChatColorsPalette.Bubbles.SUBLIME
ChatStyle.BubbleColorPreset.GRADIENT_SEA -> ChatColorsPalette.Bubbles.SEA
ChatStyle.BubbleColorPreset.GRADIENT_TANGERINE -> ChatColorsPalette.Bubbles.TANGERINE
ChatStyle.BubbleColorPreset.UNKNOWN_BUBBLE_COLOR_PRESET, ChatStyle.BubbleColorPreset.SOLID_ULTRAMARINE -> ChatColorsPalette.Bubbles.ULTRAMARINE
}
}
if (this.autoBubbleColor != null) {
return ChatColorsPalette.Bubbles.default.withId(ChatColors.Id.Auto)
}
if (this.customColorId != null) {
return importState.remoteToLocalColorId[this.customColorId]?.let { localId ->
val colorId = ChatColors.Id.forLongValue(localId)
ChatColorsPalette.Bubbles.default.withId(colorId)
}
}
return null
}
fun ChatColors.toRemote(): ChatStyle.BubbleColorPreset? {
when (this) {
// Solids
ChatColorsPalette.Bubbles.CRIMSON -> return ChatStyle.BubbleColorPreset.SOLID_CRIMSON
ChatColorsPalette.Bubbles.VERMILION -> return ChatStyle.BubbleColorPreset.SOLID_VERMILION
ChatColorsPalette.Bubbles.BURLAP -> return ChatStyle.BubbleColorPreset.SOLID_BURLAP
ChatColorsPalette.Bubbles.FOREST -> return ChatStyle.BubbleColorPreset.SOLID_FOREST
ChatColorsPalette.Bubbles.WINTERGREEN -> return ChatStyle.BubbleColorPreset.SOLID_WINTERGREEN
ChatColorsPalette.Bubbles.TEAL -> return ChatStyle.BubbleColorPreset.SOLID_TEAL
ChatColorsPalette.Bubbles.BLUE -> return ChatStyle.BubbleColorPreset.SOLID_BLUE
ChatColorsPalette.Bubbles.INDIGO -> return ChatStyle.BubbleColorPreset.SOLID_INDIGO
ChatColorsPalette.Bubbles.VIOLET -> return ChatStyle.BubbleColorPreset.SOLID_VIOLET
ChatColorsPalette.Bubbles.PLUM -> return ChatStyle.BubbleColorPreset.SOLID_PLUM
ChatColorsPalette.Bubbles.TAUPE -> return ChatStyle.BubbleColorPreset.SOLID_TAUPE
ChatColorsPalette.Bubbles.STEEL -> return ChatStyle.BubbleColorPreset.SOLID_STEEL
ChatColorsPalette.Bubbles.ULTRAMARINE -> return ChatStyle.BubbleColorPreset.SOLID_ULTRAMARINE
// Gradients
ChatColorsPalette.Bubbles.EMBER -> return ChatStyle.BubbleColorPreset.GRADIENT_EMBER
ChatColorsPalette.Bubbles.MIDNIGHT -> return ChatStyle.BubbleColorPreset.GRADIENT_MIDNIGHT
ChatColorsPalette.Bubbles.INFRARED -> return ChatStyle.BubbleColorPreset.GRADIENT_INFRARED
ChatColorsPalette.Bubbles.LAGOON -> return ChatStyle.BubbleColorPreset.GRADIENT_LAGOON
ChatColorsPalette.Bubbles.FLUORESCENT -> return ChatStyle.BubbleColorPreset.GRADIENT_FLUORESCENT
ChatColorsPalette.Bubbles.BASIL -> return ChatStyle.BubbleColorPreset.GRADIENT_BASIL
ChatColorsPalette.Bubbles.SUBLIME -> return ChatStyle.BubbleColorPreset.GRADIENT_SUBLIME
ChatColorsPalette.Bubbles.SEA -> return ChatStyle.BubbleColorPreset.GRADIENT_SEA
ChatColorsPalette.Bubbles.TANGERINE -> return ChatStyle.BubbleColorPreset.GRADIENT_TANGERINE
}
return null
}
fun ChatStyle.WallpaperPreset.toLocal(): ChatWallpaper? {
return when (this) {
ChatStyle.WallpaperPreset.SOLID_BLUSH -> SingleColorChatWallpaper.BLUSH
ChatStyle.WallpaperPreset.SOLID_COPPER -> SingleColorChatWallpaper.COPPER
ChatStyle.WallpaperPreset.SOLID_DUST -> SingleColorChatWallpaper.DUST
ChatStyle.WallpaperPreset.SOLID_CELADON -> SingleColorChatWallpaper.CELADON
ChatStyle.WallpaperPreset.SOLID_RAINFOREST -> SingleColorChatWallpaper.RAINFOREST
ChatStyle.WallpaperPreset.SOLID_PACIFIC -> SingleColorChatWallpaper.PACIFIC
ChatStyle.WallpaperPreset.SOLID_FROST -> SingleColorChatWallpaper.FROST
ChatStyle.WallpaperPreset.SOLID_NAVY -> SingleColorChatWallpaper.NAVY
ChatStyle.WallpaperPreset.SOLID_LILAC -> SingleColorChatWallpaper.LILAC
ChatStyle.WallpaperPreset.SOLID_PINK -> SingleColorChatWallpaper.PINK
ChatStyle.WallpaperPreset.SOLID_EGGPLANT -> SingleColorChatWallpaper.EGGPLANT
ChatStyle.WallpaperPreset.SOLID_SILVER -> SingleColorChatWallpaper.SILVER
ChatStyle.WallpaperPreset.GRADIENT_SUNSET -> GradientChatWallpaper.SUNSET
ChatStyle.WallpaperPreset.GRADIENT_NOIR -> GradientChatWallpaper.NOIR
ChatStyle.WallpaperPreset.GRADIENT_HEATMAP -> GradientChatWallpaper.HEATMAP
ChatStyle.WallpaperPreset.GRADIENT_AQUA -> GradientChatWallpaper.AQUA
ChatStyle.WallpaperPreset.GRADIENT_IRIDESCENT -> GradientChatWallpaper.IRIDESCENT
ChatStyle.WallpaperPreset.GRADIENT_MONSTERA -> GradientChatWallpaper.MONSTERA
ChatStyle.WallpaperPreset.GRADIENT_BLISS -> GradientChatWallpaper.BLISS
ChatStyle.WallpaperPreset.GRADIENT_SKY -> GradientChatWallpaper.SKY
ChatStyle.WallpaperPreset.GRADIENT_PEACH -> GradientChatWallpaper.PEACH
else -> null
}
}
private fun Int.toRemoteWallpaperPreset(): ChatStyle.WallpaperPreset {
return when (this) {
SingleColorChatWallpaper.BLUSH.color -> ChatStyle.WallpaperPreset.SOLID_BLUSH
SingleColorChatWallpaper.COPPER.color -> ChatStyle.WallpaperPreset.SOLID_COPPER
SingleColorChatWallpaper.DUST.color -> ChatStyle.WallpaperPreset.SOLID_DUST
SingleColorChatWallpaper.CELADON.color -> ChatStyle.WallpaperPreset.SOLID_CELADON
SingleColorChatWallpaper.RAINFOREST.color -> ChatStyle.WallpaperPreset.SOLID_RAINFOREST
SingleColorChatWallpaper.PACIFIC.color -> ChatStyle.WallpaperPreset.SOLID_PACIFIC
SingleColorChatWallpaper.FROST.color -> ChatStyle.WallpaperPreset.SOLID_FROST
SingleColorChatWallpaper.NAVY.color -> ChatStyle.WallpaperPreset.SOLID_NAVY
SingleColorChatWallpaper.LILAC.color -> ChatStyle.WallpaperPreset.SOLID_LILAC
SingleColorChatWallpaper.PINK.color -> ChatStyle.WallpaperPreset.SOLID_PINK
SingleColorChatWallpaper.EGGPLANT.color -> ChatStyle.WallpaperPreset.SOLID_EGGPLANT
SingleColorChatWallpaper.SILVER.color -> ChatStyle.WallpaperPreset.SOLID_SILVER
else -> ChatStyle.WallpaperPreset.UNKNOWN_WALLPAPER_PRESET
}
}
private fun Wallpaper.LinearGradient.toRemoteWallpaperPreset(): ChatStyle.WallpaperPreset {
val colorArray = colors.toIntArray()
return when {
colorArray contentEquals GradientChatWallpaper.SUNSET.colors -> ChatStyle.WallpaperPreset.GRADIENT_SUNSET
colorArray contentEquals GradientChatWallpaper.NOIR.colors -> ChatStyle.WallpaperPreset.GRADIENT_NOIR
colorArray contentEquals GradientChatWallpaper.HEATMAP.colors -> ChatStyle.WallpaperPreset.GRADIENT_HEATMAP
colorArray contentEquals GradientChatWallpaper.AQUA.colors -> ChatStyle.WallpaperPreset.GRADIENT_AQUA
colorArray contentEquals GradientChatWallpaper.IRIDESCENT.colors -> ChatStyle.WallpaperPreset.GRADIENT_IRIDESCENT
colorArray contentEquals GradientChatWallpaper.MONSTERA.colors -> ChatStyle.WallpaperPreset.GRADIENT_MONSTERA
colorArray contentEquals GradientChatWallpaper.BLISS.colors -> ChatStyle.WallpaperPreset.GRADIENT_BLISS
colorArray contentEquals GradientChatWallpaper.SKY.colors -> ChatStyle.WallpaperPreset.GRADIENT_SKY
colorArray contentEquals GradientChatWallpaper.PEACH.colors -> ChatStyle.WallpaperPreset.GRADIENT_PEACH
else -> ChatStyle.WallpaperPreset.UNKNOWN_WALLPAPER_PRESET
}
}
private fun Wallpaper.File.toFilePointer(readableDatabase: SQLiteDatabase): FilePointer? {
val attachmentId: AttachmentId = UriUtil.parseOrNull(this.uri)?.let { PartUriParser(it).partId } ?: return null
val wallpaperAttachment: ArchiveAttachmentData? = readableDatabase
.select(
AttachmentTable.ARCHIVE_MEDIA_NAME,
AttachmentTable.ARCHIVE_CDN,
AttachmentTable.REMOTE_KEY,
AttachmentTable.REMOTE_DIGEST,
AttachmentTable.DATA_SIZE,
AttachmentTable.CONTENT_TYPE,
AttachmentTable.WIDTH,
AttachmentTable.HEIGHT
)
.from(AttachmentTable.TABLE_NAME)
.where("${AttachmentTable.ID} = ?", attachmentId.id)
.run()
.readToSingleObject { cursor -> cursor.toArchiveAttachmentData() }
return wallpaperAttachment?.let { attachment ->
FilePointer(
backupLocator = FilePointer.BackupLocator(
mediaName = attachment.archiveMediaName ?: "",
cdnNumber = attachment.archiveCdn,
key = attachment.remoteKey?.toByteString() ?: ByteString.EMPTY,
size = attachment.size.toInt(),
digest = attachment.remoteDigest?.toByteString() ?: ByteString.EMPTY
),
contentType = attachment.contentType,
width = attachment.width,
height = attachment.height
)
}
}
private fun Cursor.toArchiveAttachmentData(): ArchiveAttachmentData {
return ArchiveAttachmentData(
archiveMediaName = this.requireString(AttachmentTable.ARCHIVE_MEDIA_NAME),
archiveCdn = this.requireInt(AttachmentTable.ARCHIVE_CDN),
remoteKey = this.requireString(AttachmentTable.REMOTE_KEY)?.let { Base64.decodeOrNull(it) },
remoteDigest = this.requireBlob(AttachmentTable.REMOTE_DIGEST),
size = this.requireLong(AttachmentTable.DATA_SIZE),
contentType = this.requireString(AttachmentTable.CONTENT_TYPE),
width = this.requireInt(AttachmentTable.WIDTH),
height = this.requireInt(AttachmentTable.HEIGHT)
)
}
private class ArchiveAttachmentData(
val archiveMediaName: String?,
val archiveCdn: Int,
val remoteKey: ByteArray?,
val remoteDigest: ByteArray?,
val size: Long,
val contentType: String?,
val width: Int,
val height: Int
)

View File

@@ -1,112 +0,0 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2.util
import org.thoughtcrime.securesms.backup.v2.ImportState
import org.thoughtcrime.securesms.backup.v2.proto.ChatStyle
import org.thoughtcrime.securesms.conversation.colors.ChatColors
import org.thoughtcrime.securesms.conversation.colors.ChatColorsPalette
// TODO [backup] Passing in chatColorId probably unnecessary. Only stored as separate column in recipient table for querying, I believe.
object BackupConverters {
fun constructRemoteChatStyle(chatColors: ChatColors?, chatColorId: ChatColors.Id): ChatStyle? {
var chatStyleBuilder: ChatStyle.Builder? = null
if (chatColors != null) {
chatStyleBuilder = ChatStyle.Builder()
when (chatColorId) {
ChatColors.Id.NotSet -> {}
ChatColors.Id.Auto -> {
chatStyleBuilder.autoBubbleColor = ChatStyle.AutomaticBubbleColor()
}
ChatColors.Id.BuiltIn -> {
chatStyleBuilder.bubbleColorPreset = chatColors.toRemote()
}
is ChatColors.Id.Custom -> {
chatStyleBuilder.customColorId = chatColorId.longValue
}
}
}
// TODO [backup] wallpaper
return chatStyleBuilder?.build()
}
}
fun ChatStyle.toLocal(importState: ImportState): ChatColors? {
if (this.bubbleColorPreset != null) {
return when (this.bubbleColorPreset) {
ChatStyle.BubbleColorPreset.SOLID_CRIMSON -> ChatColorsPalette.Bubbles.CRIMSON
ChatStyle.BubbleColorPreset.SOLID_VERMILION -> ChatColorsPalette.Bubbles.VERMILION
ChatStyle.BubbleColorPreset.SOLID_BURLAP -> ChatColorsPalette.Bubbles.BURLAP
ChatStyle.BubbleColorPreset.SOLID_FOREST -> ChatColorsPalette.Bubbles.FOREST
ChatStyle.BubbleColorPreset.SOLID_WINTERGREEN -> ChatColorsPalette.Bubbles.WINTERGREEN
ChatStyle.BubbleColorPreset.SOLID_TEAL -> ChatColorsPalette.Bubbles.TEAL
ChatStyle.BubbleColorPreset.SOLID_BLUE -> ChatColorsPalette.Bubbles.BLUE
ChatStyle.BubbleColorPreset.SOLID_INDIGO -> ChatColorsPalette.Bubbles.INDIGO
ChatStyle.BubbleColorPreset.SOLID_VIOLET -> ChatColorsPalette.Bubbles.VIOLET
ChatStyle.BubbleColorPreset.SOLID_PLUM -> ChatColorsPalette.Bubbles.PLUM
ChatStyle.BubbleColorPreset.SOLID_TAUPE -> ChatColorsPalette.Bubbles.TAUPE
ChatStyle.BubbleColorPreset.SOLID_STEEL -> ChatColorsPalette.Bubbles.STEEL
ChatStyle.BubbleColorPreset.GRADIENT_EMBER -> ChatColorsPalette.Bubbles.EMBER
ChatStyle.BubbleColorPreset.GRADIENT_MIDNIGHT -> ChatColorsPalette.Bubbles.MIDNIGHT
ChatStyle.BubbleColorPreset.GRADIENT_INFRARED -> ChatColorsPalette.Bubbles.INFRARED
ChatStyle.BubbleColorPreset.GRADIENT_LAGOON -> ChatColorsPalette.Bubbles.LAGOON
ChatStyle.BubbleColorPreset.GRADIENT_FLUORESCENT -> ChatColorsPalette.Bubbles.FLUORESCENT
ChatStyle.BubbleColorPreset.GRADIENT_BASIL -> ChatColorsPalette.Bubbles.BASIL
ChatStyle.BubbleColorPreset.GRADIENT_SUBLIME -> ChatColorsPalette.Bubbles.SUBLIME
ChatStyle.BubbleColorPreset.GRADIENT_SEA -> ChatColorsPalette.Bubbles.SEA
ChatStyle.BubbleColorPreset.GRADIENT_TANGERINE -> ChatColorsPalette.Bubbles.TANGERINE
ChatStyle.BubbleColorPreset.UNKNOWN_BUBBLE_COLOR_PRESET, ChatStyle.BubbleColorPreset.SOLID_ULTRAMARINE -> ChatColorsPalette.Bubbles.ULTRAMARINE
}
}
if (this.autoBubbleColor != null) {
return ChatColorsPalette.Bubbles.default.withId(ChatColors.Id.Auto)
}
if (this.customColorId != null) {
return importState.remoteToLocalColorId[this.customColorId]?.let { localId ->
val colorId = ChatColors.Id.forLongValue(localId)
ChatColorsPalette.Bubbles.default.withId(colorId)
}
}
return null
}
fun ChatColors.toRemote(): ChatStyle.BubbleColorPreset? {
when (this) {
// Solids
ChatColorsPalette.Bubbles.CRIMSON -> return ChatStyle.BubbleColorPreset.SOLID_CRIMSON
ChatColorsPalette.Bubbles.VERMILION -> return ChatStyle.BubbleColorPreset.SOLID_VERMILION
ChatColorsPalette.Bubbles.BURLAP -> return ChatStyle.BubbleColorPreset.SOLID_BURLAP
ChatColorsPalette.Bubbles.FOREST -> return ChatStyle.BubbleColorPreset.SOLID_FOREST
ChatColorsPalette.Bubbles.WINTERGREEN -> return ChatStyle.BubbleColorPreset.SOLID_WINTERGREEN
ChatColorsPalette.Bubbles.TEAL -> return ChatStyle.BubbleColorPreset.SOLID_TEAL
ChatColorsPalette.Bubbles.BLUE -> return ChatStyle.BubbleColorPreset.SOLID_BLUE
ChatColorsPalette.Bubbles.INDIGO -> return ChatStyle.BubbleColorPreset.SOLID_INDIGO
ChatColorsPalette.Bubbles.VIOLET -> return ChatStyle.BubbleColorPreset.SOLID_VIOLET
ChatColorsPalette.Bubbles.PLUM -> return ChatStyle.BubbleColorPreset.SOLID_PLUM
ChatColorsPalette.Bubbles.TAUPE -> return ChatStyle.BubbleColorPreset.SOLID_TAUPE
ChatColorsPalette.Bubbles.STEEL -> return ChatStyle.BubbleColorPreset.SOLID_STEEL
ChatColorsPalette.Bubbles.ULTRAMARINE -> return ChatStyle.BubbleColorPreset.SOLID_ULTRAMARINE
// Gradients
ChatColorsPalette.Bubbles.EMBER -> return ChatStyle.BubbleColorPreset.GRADIENT_EMBER
ChatColorsPalette.Bubbles.MIDNIGHT -> return ChatStyle.BubbleColorPreset.GRADIENT_MIDNIGHT
ChatColorsPalette.Bubbles.INFRARED -> return ChatStyle.BubbleColorPreset.GRADIENT_INFRARED
ChatColorsPalette.Bubbles.LAGOON -> return ChatStyle.BubbleColorPreset.GRADIENT_LAGOON
ChatColorsPalette.Bubbles.FLUORESCENT -> return ChatStyle.BubbleColorPreset.GRADIENT_FLUORESCENT
ChatColorsPalette.Bubbles.BASIL -> return ChatStyle.BubbleColorPreset.GRADIENT_BASIL
ChatColorsPalette.Bubbles.SUBLIME -> return ChatStyle.BubbleColorPreset.GRADIENT_SUBLIME
ChatColorsPalette.Bubbles.SEA -> return ChatStyle.BubbleColorPreset.GRADIENT_SEA
ChatColorsPalette.Bubbles.TANGERINE -> return ChatStyle.BubbleColorPreset.GRADIENT_TANGERINE
}
return null
}

View File

@@ -33,6 +33,7 @@ class ChatColors(
) : Parcelable {
fun isGradient(): Boolean = linearGradient != null
fun isSolid(): Boolean = singleColor != null
/**
* Returns the Drawable to render the linear gradient, or null if this ChatColors is a single color.

View File

@@ -70,6 +70,7 @@ import org.thoughtcrime.securesms.attachments.Attachment
import org.thoughtcrime.securesms.attachments.AttachmentId
import org.thoughtcrime.securesms.attachments.Cdn
import org.thoughtcrime.securesms.attachments.DatabaseAttachment
import org.thoughtcrime.securesms.attachments.WallpaperAttachment
import org.thoughtcrime.securesms.audio.AudioHash
import org.thoughtcrime.securesms.blurhash.BlurHash
import org.thoughtcrime.securesms.crypto.AttachmentSecret
@@ -182,6 +183,7 @@ class AttachmentTable(
const val TRANSFER_RESTORE_IN_PROGRESS = 6
const val TRANSFER_RESTORE_OFFLOADED = 7
const val PREUPLOAD_MESSAGE_ID: Long = -8675309
const val WALLPAPER_MESSAGE_ID: Long = -8675308
private val PROJECTION = arrayOf(
ID,
@@ -816,7 +818,7 @@ class AttachmentTable(
fun trimAllAbandonedAttachments() {
val deleteCount = writableDatabase
.delete(TABLE_NAME)
.where("$MESSAGE_ID != $PREUPLOAD_MESSAGE_ID AND $MESSAGE_ID NOT IN (SELECT ${MessageTable.ID} FROM ${MessageTable.TABLE_NAME})")
.where("$MESSAGE_ID != $PREUPLOAD_MESSAGE_ID AND $MESSAGE_ID != $WALLPAPER_MESSAGE_ID AND $MESSAGE_ID NOT IN (SELECT ${MessageTable.ID} FROM ${MessageTable.TABLE_NAME})")
.run()
if (deleteCount > 0) {
@@ -2184,6 +2186,16 @@ class AttachmentTable(
throw MmsException(e)
}
return insertAttachmentWithData(messageId, dataStream, attachment, quote)
}
/**
* Inserts an attachment with existing data. This is likely an outgoing attachment that we're in the process of sending.
*
* @param dataStream The stream to read the data from. This stream will be closed by this method.
*/
@Throws(MmsException::class)
private fun insertAttachmentWithData(messageId: Long, dataStream: InputStream, attachment: Attachment, quote: Boolean): AttachmentId {
// To avoid performing long-running operations in a transaction, we write the data to an independent file first in a way that doesn't rely on db state.
val fileWriteResult: DataFileWriteResult = writeToDataFile(newDataFile(context), dataStream, attachment.transformProperties ?: TransformProperties.empty())
Log.d(TAG, "[insertAttachmentWithData] Wrote data to file: ${fileWriteResult.file.absolutePath} (MessageId: $messageId, ${attachment.uri})")
@@ -2212,12 +2224,16 @@ class AttachmentTable(
}
if (hashMatch != null) {
if (fileWriteResult.hash == hashMatch.hashStart) {
Log.i(TAG, "[insertAttachmentWithData] Found that the new attachment hash matches the DATA_HASH_START of ${hashMatch.id}. Using all of it's fields. (MessageId: $messageId, ${attachment.uri})")
} else if (fileWriteResult.hash == hashMatch.hashEnd) {
Log.i(TAG, "[insertAttachmentWithData] Found that the new attachment hash matches the DATA_HASH_END of ${hashMatch.id}. Using all of it's fields. (MessageId: $messageId, ${attachment.uri})")
} else {
throw IllegalStateException("Should not be possible based on query.")
when (fileWriteResult.hash) {
hashMatch.hashStart -> {
Log.i(TAG, "[insertAttachmentWithData] Found that the new attachment hash matches the DATA_HASH_START of ${hashMatch.id}. Using all of it's fields. (MessageId: $messageId, ${attachment.uri})")
}
hashMatch.hashEnd -> {
Log.i(TAG, "[insertAttachmentWithData] Found that the new attachment hash matches the DATA_HASH_END of ${hashMatch.id}. Using all of it's fields. (MessageId: $messageId, ${attachment.uri})")
}
else -> {
throw IllegalStateException("Should not be possible based on query.")
}
}
contentValues.put(DATA_FILE, hashMatch.file.absolutePath)
@@ -2309,6 +2325,21 @@ class AttachmentTable(
return attachmentId
}
fun insertWallpaper(dataStream: InputStream): AttachmentId {
return insertAttachmentWithData(WALLPAPER_MESSAGE_ID, dataStream, WallpaperAttachment(), quote = false).also { id ->
createKeyIvIfNecessary(id)
}
}
fun getAllWallpapers(): List<AttachmentId> {
return readableDatabase
.select(ID)
.from(TABLE_NAME)
.where("$MESSAGE_ID = $WALLPAPER_MESSAGE_ID")
.run()
.readToList { AttachmentId(it.requireLong(ID)) }
}
private fun getTransferFile(db: SQLiteDatabase, attachmentId: AttachmentId): File? {
return db
.select(TRANSFER_FILE)

View File

@@ -98,6 +98,7 @@ import org.thoughtcrime.securesms.util.ProfileUtil
import org.thoughtcrime.securesms.util.RemoteConfig
import org.thoughtcrime.securesms.util.Util
import org.thoughtcrime.securesms.wallpaper.ChatWallpaper
import org.thoughtcrime.securesms.wallpaper.ChatWallpaperFactory
import org.thoughtcrime.securesms.wallpaper.WallpaperStorage
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile
import org.whispersystems.signalservice.api.push.ServiceId
@@ -1968,7 +1969,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
for (pair in idWithWallpaper) {
AppDependencies.databaseObserver.notifyRecipientChanged(pair.first)
if (pair.second != null) {
WallpaperStorage.onWallpaperDeselected(context, Uri.parse(pair.second))
WallpaperStorage.onWallpaperDeselected(Uri.parse(pair.second))
}
}
} else {
@@ -1980,11 +1981,11 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
}
}
fun setWallpaper(id: RecipientId, chatWallpaper: ChatWallpaper?) {
setWallpaper(id, chatWallpaper?.serialize())
fun setWallpaper(id: RecipientId, chatWallpaper: ChatWallpaper?, notifyDeselected: Boolean) {
setWallpaper(id, chatWallpaper?.serialize(), notifyDeselected)
}
private fun setWallpaper(id: RecipientId, wallpaper: Wallpaper?) {
private fun setWallpaper(id: RecipientId, wallpaper: Wallpaper?, notifyDeselected: Boolean) {
val existingWallpaperUri = getWallpaperUri(id)
val values = ContentValues().apply {
put(WALLPAPER, wallpaper?.encode())
@@ -1999,8 +2000,8 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
AppDependencies.databaseObserver.notifyRecipientChanged(id)
}
if (existingWallpaperUri != null) {
WallpaperStorage.onWallpaperDeselected(context, existingWallpaperUri)
if (notifyDeselected && existingWallpaperUri != null) {
WallpaperStorage.onWallpaperDeselected(existingWallpaperUri)
}
}
@@ -2010,7 +2011,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
.dimLevelInDarkTheme(if (enabled) ChatWallpaper.FIXED_DIM_LEVEL_FOR_DARK_THEME else 0f)
.build()
setWallpaper(id, updated)
setWallpaper(id, updated, false)
}
private fun getWallpaper(id: RecipientId): Wallpaper? {
@@ -2055,6 +2056,23 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
return 0
}
/**
* Migrates all recipients using [legacyUri] for their wallpaper to [newUri].
* Needed for an app migration.
*/
fun migrateWallpaperUri(legacyUri: Uri, newUri: Uri): Int {
val newWallpaper = ChatWallpaperFactory.create(newUri)
return writableDatabase
.update(TABLE_NAME)
.values(
WALLPAPER to newWallpaper.serialize().encode(),
WALLPAPER_URI to newUri.toString()
)
.where("$WALLPAPER_URI = ?", legacyUri)
.run()
}
fun getPhoneNumberDiscoverability(id: RecipientId): PhoneNumberDiscoverableState? {
return readableDatabase
.select(PHONE_NUMBER_DISCOVERABLE)

View File

@@ -66,9 +66,11 @@ class BackupRestoreMediaJob private constructor(parameters: Parameters) : BaseJo
val messageMap = SignalDatabase.messages.getMessages(messageIds).associate { it.id to (it as MmsMessageRecord) }
for (attachment in attachmentBatch) {
val isWallpaper = attachment.mmsId == AttachmentTable.WALLPAPER_MESSAGE_ID
val message = messageMap[attachment.mmsId]
if (message == null) {
Log.w(TAG, "Unable to find message for ${attachment.attachmentId}")
if (message == null && !isWallpaper) {
Log.w(TAG, "Unable to find message for ${attachment.attachmentId}, mmsId: ${attachment.mmsId}")
notRestorable += attachment
continue
}
@@ -79,7 +81,7 @@ class BackupRestoreMediaJob private constructor(parameters: Parameters) : BaseJo
highPriority = false
)
if (shouldRestoreFullSize(message, restoreTime, SignalStore.backup.optimizeStorage)) {
if (isWallpaper || shouldRestoreFullSize(message!!, restoreTime, SignalStore.backup.optimizeStorage)) {
restoreFullAttachmentJobs += attachment to RestoreAttachmentJob(
messageId = attachment.mmsId,
attachmentId = attachment.attachmentId

View File

@@ -94,6 +94,7 @@ import org.thoughtcrime.securesms.migrations.TrimByLengthSettingsMigrationJob;
import org.thoughtcrime.securesms.migrations.UpdateSmsJobsMigrationJob;
import org.thoughtcrime.securesms.migrations.UserNotificationMigrationJob;
import org.thoughtcrime.securesms.migrations.UuidMigrationJob;
import org.thoughtcrime.securesms.migrations.WallpaperStorageMigrationJob;
import java.util.Arrays;
import java.util.HashMap;
@@ -309,6 +310,7 @@ public final class JobManagerFactories {
put(UpdateSmsJobsMigrationJob.KEY, new UpdateSmsJobsMigrationJob.Factory());
put(UserNotificationMigrationJob.KEY, new UserNotificationMigrationJob.Factory());
put(UuidMigrationJob.KEY, new UuidMigrationJob.Factory());
put(WallpaperStorageMigrationJob.KEY, new WallpaperStorageMigrationJob.Factory());
// Dead jobs
put(FailingJob.KEY, new FailingJob.Factory());

View File

@@ -1,6 +1,5 @@
package org.thoughtcrime.securesms.keyvalue;
import android.content.Context;
import android.net.Uri;
import androidx.annotation.NonNull;
@@ -35,8 +34,8 @@ public final class WallpaperValues extends SignalStoreValues {
return Collections.emptyList();
}
public void setWallpaper(@NonNull Context context, @Nullable ChatWallpaper wallpaper) {
Wallpaper currentWallpaper = getCurrentWallpaper();
public void setWallpaper(@Nullable ChatWallpaper wallpaper) {
Wallpaper currentWallpaper = getCurrentRawWallpaper();
Uri currentUri = null;
if (currentWallpaper != null && currentWallpaper.file_ != null) {
@@ -50,12 +49,12 @@ public final class WallpaperValues extends SignalStoreValues {
}
if (currentUri != null) {
WallpaperStorage.onWallpaperDeselected(context, currentUri);
WallpaperStorage.onWallpaperDeselected(currentUri);
}
}
public @Nullable ChatWallpaper getWallpaper() {
Wallpaper currentWallpaper = getCurrentWallpaper();
Wallpaper currentWallpaper = getCurrentRawWallpaper();
if (currentWallpaper != null) {
return ChatWallpaperFactory.create(currentWallpaper);
@@ -69,7 +68,7 @@ public final class WallpaperValues extends SignalStoreValues {
}
public void setDimInDarkTheme(boolean enabled) {
Wallpaper currentWallpaper = getCurrentWallpaper();
Wallpaper currentWallpaper = getCurrentRawWallpaper();
if (currentWallpaper != null) {
putBlob(KEY_WALLPAPER,
@@ -88,7 +87,7 @@ public final class WallpaperValues extends SignalStoreValues {
* wallpaper is both set *and* it's an image.
*/
public @Nullable Uri getWallpaperUri() {
Wallpaper currentWallpaper = getCurrentWallpaper();
Wallpaper currentWallpaper = getCurrentRawWallpaper();
if (currentWallpaper != null && currentWallpaper.file_ != null) {
return Uri.parse(currentWallpaper.file_.uri);
@@ -97,7 +96,10 @@ public final class WallpaperValues extends SignalStoreValues {
}
}
private @Nullable Wallpaper getCurrentWallpaper() {
/**
* Allows for retrieval of the raw, serialized wallpaper proto. Clients should prefer {@link #getWallpaper()} instead.
*/
public @Nullable Wallpaper getCurrentRawWallpaper() {
byte[] serialized = getBlob(KEY_WALLPAPER, null);
if (serialized != null) {
@@ -111,4 +113,12 @@ public final class WallpaperValues extends SignalStoreValues {
return null;
}
}
/**
* For a migration, we need to update the current wallpaper _without_ triggering the onDeselectedEvents and such.
* For normal usage, use {@link #setWallpaper(ChatWallpaper)}
*/
public void setRawWallpaperForMigration(@NonNull Wallpaper wallpaper) {
putBlob(KEY_WALLPAPER, wallpaper.encode());
}
}

View File

@@ -156,9 +156,10 @@ public class ApplicationMigrations {
// static final int BACKFILL_DIGESTS = 112;
static final int BACKFILL_DIGESTS_V2 = 113;
static final int CALL_LINK_STORAGE_SYNC = 114;
static final int WALLPAPER_MIGRATION = 115;
}
public static final int CURRENT_VERSION = 114;
public static final int CURRENT_VERSION = 115;
/**
* This *must* be called after the {@link JobManager} has been instantiated, but *before* the call
@@ -713,6 +714,10 @@ public class ApplicationMigrations {
jobs.put(Version.CALL_LINK_STORAGE_SYNC, new SyncCallLinksMigrationJob());
}
if (lastSeenVersion < Version.WALLPAPER_MIGRATION) {
jobs.put(Version.WALLPAPER_MIGRATION, new WallpaperStorageMigrationJob());
}
return jobs;
}

View File

@@ -0,0 +1,88 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.migrations
import android.content.Context
import android.net.Uri
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.BuildConfig
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.databaseprotos.Wallpaper
import org.thoughtcrime.securesms.jobmanager.Job
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.mms.PartAuthority
import org.thoughtcrime.securesms.util.storage.FileStorage
import java.io.File
import java.io.IOException
/**
* We need to move the wallpapers to be stored in the attachment table as part of backups V2.
*/
internal class WallpaperStorageMigrationJob(parameters: Parameters = Parameters.Builder().build()) : MigrationJob(parameters) {
companion object {
private val TAG = Log.tag(WallpaperStorageMigrationJob::class.java)
const val KEY = "WallpaperStorageMigrationJob"
private const val DIRECTORY = "wallpapers"
private const val FILENAME_BASE = "wallpaper"
private val CONTENT_URI = Uri.parse("content://${BuildConfig.APPLICATION_ID}/wallpaper")
}
override fun getFactoryKey(): String = KEY
override fun isUiBlocking(): Boolean = true
override fun performMigration() {
val wallpaperFileNames = FileStorage.getAll(context, DIRECTORY, FILENAME_BASE)
if (wallpaperFileNames.isEmpty()) {
Log.i(TAG, "No wallpapers to migrate. Done.")
return
}
Log.i(TAG, "There are ${wallpaperFileNames.size} wallpapers to migrate.")
val currentDefaultWallpaperUri = SignalStore.wallpaper.currentRawWallpaper?.file_?.uri
for (filename in wallpaperFileNames) {
val inputStream = FileStorage.read(context, DIRECTORY, filename)
val wallpaperAttachmentId = SignalDatabase.attachments.insertWallpaper(inputStream)
val directory = context.getDir(DIRECTORY, Context.MODE_PRIVATE)
val file = File(directory, filename)
val legacyUri = Uri.withAppendedPath(CONTENT_URI, filename)
val newUri = PartAuthority.getAttachmentDataUri(wallpaperAttachmentId)
val updatedUserCount = SignalDatabase.recipients.migrateWallpaperUri(
legacyUri = legacyUri,
newUri = newUri
)
Log.d(TAG, "Wallpaper with name '$filename' was in use by $updatedUserCount recipients.")
if (currentDefaultWallpaperUri == legacyUri.toString()) {
Log.d(TAG, "Wallpaper with name '$filename' was set as the default wallpaper. Updating.")
SignalStore.wallpaper.setRawWallpaperForMigration(Wallpaper(file_ = Wallpaper.File(uri = newUri.toString())))
}
val deleted = file.delete()
if (!deleted) {
Log.w(TAG, "Failed to delete wallpaper file: $file")
}
}
Log.i(TAG, "Successfully migrated ${wallpaperFileNames.size} wallpapers.")
}
override fun shouldRetry(e: Exception): Boolean = e is IOException
class Factory : Job.Factory<WallpaperStorageMigrationJob> {
override fun create(parameters: Parameters, serializedData: ByteArray?): WallpaperStorageMigrationJob {
return WallpaperStorageMigrationJob(parameters)
}
}
}

View File

@@ -34,13 +34,11 @@ public class PartAuthority {
private static final String PART_URI_STRING = "content://" + AUTHORITY + "/part";
private static final String PART_THUMBNAIL_STRING = "content://" + AUTHORITY + "/thumbnail";
private static final String STICKER_URI_STRING = "content://" + AUTHORITY + "/sticker";
private static final String WALLPAPER_URI_STRING = "content://" + AUTHORITY + "/wallpaper";
private static final String EMOJI_URI_STRING = "content://" + AUTHORITY + "/emoji";
private static final String AVATAR_PICKER_URI_STRING = "content://" + AUTHORITY + "/avatar_picker";
private static final Uri PART_CONTENT_URI = Uri.parse(PART_URI_STRING);
private static final Uri PART_THUMBNAIL_URI = Uri.parse(PART_THUMBNAIL_STRING);
private static final Uri STICKER_CONTENT_URI = Uri.parse(STICKER_URI_STRING);
private static final Uri WALLPAPER_CONTENT_URI = Uri.parse(WALLPAPER_URI_STRING);
private static final Uri EMOJI_CONTENT_URI = Uri.parse(EMOJI_URI_STRING);
private static final Uri AVATAR_PICKER_CONTENT_URI = Uri.parse(AVATAR_PICKER_URI_STRING);
@@ -84,7 +82,6 @@ public class PartAuthority {
case STICKER_ROW: return SignalDatabase.stickers().getStickerStream(ContentUris.parseId(uri));
case PERSISTENT_ROW: return DeprecatedPersistentBlobProvider.getInstance(context).getStream(context, ContentUris.parseId(uri));
case BLOB_ROW: return BlobProvider.getInstance().getStream(context, uri);
case WALLPAPER_ROW: return WallpaperStorage.read(context, getWallpaperFilename(uri));
case EMOJI_ROW: return EmojiFiles.openForReading(context, getEmojiFilename(uri));
case AVATAR_PICKER_ROW: return AvatarPickerStorage.read(context, getAvatarPickerFilename(uri));
case THUMBNAIL_ROW: return SignalDatabase.attachments().getAttachmentThumbnailStream(new PartUriParser(uri).getPartId(), 0);
@@ -190,10 +187,6 @@ public class PartAuthority {
return ContentUris.withAppendedId(STICKER_CONTENT_URI, id);
}
public static Uri getWallpaperUri(String filename) {
return Uri.withAppendedPath(WALLPAPER_CONTENT_URI, filename);
}
public static Uri getAvatarPickerUri(String filename) {
return Uri.withAppendedPath(AVATAR_PICKER_CONTENT_URI, filename);
}

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.util
import com.google.protobuf.InvalidProtocolBufferException
import com.squareup.wire.ProtoAdapter
/**
* Performs the common pattern of attempting to decode a serialized proto and returning null if it fails to decode.
*/
fun <E> ProtoAdapter<E>.decodeOrNull(serialized: ByteArray): E? {
return try {
this.decode(serialized)
} catch (e: InvalidProtocolBufferException) {
null
}
}

View File

@@ -1,33 +0,0 @@
package org.thoughtcrime.securesms.util;
import android.content.ContentResolver;
import android.content.Context;
import android.net.Uri;
import androidx.annotation.NonNull;
import java.io.File;
import java.io.IOException;
public final class UriUtil {
/**
* Ensures that an external URI is valid and doesn't contain any references to internal files or
* any other trickiness.
*/
public static boolean isValidExternalUri(@NonNull Context context, @NonNull Uri uri) {
if (ContentResolver.SCHEME_FILE.equals(uri.getScheme())) {
try {
File file = new File(uri.getPath());
return file.getCanonicalPath().equals(file.getPath()) &&
!file.getCanonicalPath().startsWith("/data") &&
!file.getCanonicalPath().contains(context.getPackageName());
} catch (IOException e) {
return false;
}
} else {
return true;
}
}
}

View File

@@ -0,0 +1,47 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.util
import android.content.ContentResolver
import android.content.Context
import android.net.Uri
import java.io.File
import java.io.IOException
object UriUtil {
/**
* Ensures that an external URI is valid and doesn't contain any references to internal files or
* any other trickiness.
*/
@JvmStatic
fun isValidExternalUri(context: Context, uri: Uri): Boolean {
if (ContentResolver.SCHEME_FILE == uri.scheme) {
try {
val file = File(uri.path)
return file.canonicalPath == file.path &&
!file.canonicalPath.startsWith("/data") &&
!file.canonicalPath.contains(context.packageName)
} catch (e: IOException) {
return false
}
} else {
return true
}
}
/**
* Parses a string to a URI if it's valid, otherwise null.
*/
fun parseOrNull(uri: String): Uri? {
return try {
Uri.parse(uri)
} catch (e: Exception) {
null
}
}
}

View File

@@ -9,7 +9,6 @@ import org.signal.core.util.concurrent.SignalExecutors;
import org.thoughtcrime.securesms.conversation.colors.ChatColors;
import org.thoughtcrime.securesms.conversation.colors.ChatColorsPalette;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
@@ -50,7 +49,7 @@ class ChatWallpaperRepository {
EXECUTOR.execute(() -> {
List<ChatWallpaper> wallpapers = new ArrayList<>(ChatWallpaper.BuiltIns.INSTANCE.getAllBuiltIns());
wallpapers.addAll(WallpaperStorage.getAll(AppDependencies.getApplication()));
wallpapers.addAll(WallpaperStorage.getAll());
consumer.accept(wallpapers);
});
}
@@ -59,17 +58,17 @@ class ChatWallpaperRepository {
if (recipientId != null) {
//noinspection CodeBlock2Expr
EXECUTOR.execute(() -> {
SignalDatabase.recipients().setWallpaper(recipientId, chatWallpaper);
SignalDatabase.recipients().setWallpaper(recipientId, chatWallpaper, true);
onWallpaperSaved.run();
});
} else {
SignalStore.wallpaper().setWallpaper(AppDependencies.getApplication(), chatWallpaper);
SignalStore.wallpaper().setWallpaper(chatWallpaper);
onWallpaperSaved.run();
}
}
void resetAllWallpaper(@NonNull Runnable onWallpaperReset) {
SignalStore.wallpaper().setWallpaper(AppDependencies.getApplication(), null);
SignalStore.wallpaper().setWallpaper(null);
EXECUTOR.execute(() -> {
SignalDatabase.recipients().resetAllWallpaper();
onWallpaperReset.run();
@@ -95,7 +94,8 @@ class ChatWallpaperRepository {
.setWallpaper(recipientId,
ChatWallpaperFactory.updateWithDimming(recipient.getWallpaper(),
dimInDarkTheme ? ChatWallpaper.FIXED_DIM_LEVEL_FOR_DARK_THEME
: 0f));
: 0f),
false);
} else {
throw new IllegalStateException("Unexpected call to setDimInDarkTheme, no wallpaper has been set on the given recipient or globally.");
}

View File

@@ -17,42 +17,42 @@ import java.util.Objects;
public final class GradientChatWallpaper implements ChatWallpaper, Parcelable {
public static final ChatWallpaper SUNSET = new GradientChatWallpaper(168f,
new int[] { 0xFFF3DC47, 0xFFF3DA47, 0xFFF2D546, 0xFFF2CC46, 0xFFF1C146, 0xFFEFB445, 0xFFEEA544, 0xFFEC9644, 0xFFEB8743, 0xFFE97743, 0xFFE86942, 0xFFE65C41, 0xFFE55041, 0xFFE54841, 0xFFE44240, 0xFFE44040 },
new float[] { 0.0f, 0.0807f, 0.1554f, 0.225f, 0.2904f, 0.3526f, 0.4125f, 0.471f, 0.529f, 0.5875f, 0.6474f, 0.7096f, 0.775f, 0.8446f, 0.9193f, 1f },
0f);
public static final ChatWallpaper NOIR = new GradientChatWallpaper(180f,
new int[] { 0xFF16161D, 0xFF17171E, 0xFF1A1A22, 0xFF1F1F28, 0xFF26262F, 0xFF2D2D38, 0xFF353542, 0xFF3E3E4C, 0xFF474757, 0xFF4F4F61, 0xFF57576B, 0xFF5F5F74, 0xFF65657C, 0xFF6A6A82, 0xFF6D6D85, 0xFF6E6E87 },
new float[] { 0.0000f, 0.0807f, 0.1554f, 0.2250f, 0.2904f, 0.3526f, 0.4125f, 0.4710f, 0.5290f, 0.5875f, 0.6474f, 0.7096f, 0.7750f, 0.8446f, 0.9193f, 1.0000f },
0f);
public static final ChatWallpaper HEATMAP = new GradientChatWallpaper(192f,
new int[] { 0xFFF53844, 0xFFF33845, 0xFFEC3848, 0xFFE2384C, 0xFFD63851, 0xFFC73857, 0xFFB6385E, 0xFFA43866, 0xFF93376D, 0xFF813775, 0xFF70377C, 0xFF613782, 0xFF553787, 0xFF4B378B, 0xFF44378E, 0xFF42378F },
new float[] { 0.0000f, 0.0075f, 0.0292f, 0.0637f, 0.1097f, 0.1659f, 0.2310f, 0.3037f, 0.3827f, 0.4666f, 0.5541f, 0.6439f, 0.7347f, 0.8252f, 0.9141f, 1.0000f },
0f);
public static final ChatWallpaper AQUA = new GradientChatWallpaper(180f,
new int[] { 0xFF0093E9, 0xFF0294E9, 0xFF0696E7, 0xFF0D99E5, 0xFF169EE3, 0xFF21A3E0, 0xFF2DA8DD, 0xFF3AAEDA, 0xFF46B5D6, 0xFF53BBD3, 0xFF5FC0D0, 0xFF6AC5CD, 0xFF73CACB, 0xFF7ACDC9, 0xFF7ECFC7, 0xFF80D0C7 },
new float[] { 0.0000f, 0.0807f, 0.1554f, 0.2250f, 0.2904f, 0.3526f, 0.4125f, 0.4710f, 0.5290f, 0.5875f, 0.6474f, 0.7096f, 0.7750f, 0.8446f, 0.9193f, 1.0000f },
0f);
public static final ChatWallpaper IRIDESCENT = new GradientChatWallpaper(192f,
new int[] { 0xFFF04CE6, 0xFFEE4BE6, 0xFFE54AE5, 0xFFD949E5, 0xFFC946E4, 0xFFB644E3, 0xFFA141E3, 0xFF8B3FE2, 0xFF743CE1, 0xFF5E39E0, 0xFF4936DF, 0xFF3634DE, 0xFF2632DD, 0xFF1930DD, 0xFF112FDD, 0xFF0E2FDD },
new float[] { 0.0000f, 0.0807f, 0.1554f, 0.2250f, 0.2904f, 0.3526f, 0.4125f, 0.4710f, 0.5290f, 0.5875f, 0.6474f, 0.7096f, 0.7750f, 0.8446f, 0.9193f, 1.0000f },
0f);
public static final ChatWallpaper MONSTERA = new GradientChatWallpaper(180f,
new int[] { 0xFF65CDAC, 0xFF64CDAB, 0xFF60CBA8, 0xFF5BC8A3, 0xFF55C49D, 0xFF4DC096, 0xFF45BB8F, 0xFF3CB687, 0xFF33B17F, 0xFF2AAC76, 0xFF21A76F, 0xFF1AA268, 0xFF139F62, 0xFF0E9C5E, 0xFF0B9A5B, 0xFF0A995A },
new float[] { 0.0000f, 0.0807f, 0.1554f, 0.2250f, 0.2904f, 0.3526f, 0.4125f, 0.4710f, 0.5290f, 0.5875f, 0.6474f, 0.7096f, 0.7750f, 0.8446f, 0.9193f, 1.0000f },
0f);
public static final ChatWallpaper BLISS = new GradientChatWallpaper(180f,
new int[] { 0xFFD8E1FA, 0xFFD8E0F9, 0xFFD8DEF7, 0xFFD8DBF3, 0xFFD8D6EE, 0xFFD7D1E8, 0xFFD7CCE2, 0xFFD7C6DB, 0xFFD7BFD4, 0xFFD7B9CD, 0xFFD6B4C7, 0xFFD6AFC1, 0xFFD6AABC, 0xFFD6A7B8, 0xFFD6A5B6, 0xFFD6A4B5 },
new float[] { 0.0000f, 0.0807f, 0.1554f, 0.2250f, 0.2904f, 0.3526f, 0.4125f, 0.4710f, 0.5290f, 0.5875f, 0.6474f, 0.7096f, 0.7750f, 0.8446f, 0.9193f, 1.0000f },
0f);
public static final ChatWallpaper SKY = new GradientChatWallpaper(180f,
new int[] { 0xFFD8EBFD, 0xFFD7EAFD, 0xFFD5E9FD, 0xFFD2E7FD, 0xFFCDE5FD, 0xFFC8E3FD, 0xFFC3E0FD, 0xFFBDDDFC, 0xFFB7DAFC, 0xFFB2D7FC, 0xFFACD4FC, 0xFFA7D1FC, 0xFFA3CFFB, 0xFFA0CDFB, 0xFF9ECCFB, 0xFF9DCCFB },
new float[] { 0.0000f, 0.0807f, 0.1554f, 0.2250f, 0.2904f, 0.3526f, 0.4125f, 0.4710f, 0.5290f, 0.5875f, 0.6474f, 0.7096f, 0.7750f, 0.8446f, 0.9193f, 1.0000f },
0f);
public static final ChatWallpaper PEACH = new GradientChatWallpaper(192f,
new int[] { 0xFFFFE5C2, 0xFFFFE4C1, 0xFFFFE2BF, 0xFFFFDFBD, 0xFFFEDBB9, 0xFFFED6B5, 0xFFFED1B1, 0xFFFDCCAC, 0xFFFDC6A8, 0xFFFDC0A3, 0xFFFCBB9F, 0xFFFCB69B, 0xFFFCB297, 0xFFFCAF95, 0xFFFCAD93, 0xFFFCAC92 },
new float[] { 0.0000f, 0.0807f, 0.1554f, 0.2250f, 0.2904f, 0.3526f, 0.4125f, 0.4710f, 0.5290f, 0.5875f, 0.6474f, 0.7096f, 0.7750f, 0.8446f, 0.9193f, 1.0000f },
0f);
public static final GradientChatWallpaper SUNSET = new GradientChatWallpaper(168f,
new int[] { 0xFFF3DC47, 0xFFF3DA47, 0xFFF2D546, 0xFFF2CC46, 0xFFF1C146, 0xFFEFB445, 0xFFEEA544, 0xFFEC9644, 0xFFEB8743, 0xFFE97743, 0xFFE86942, 0xFFE65C41, 0xFFE55041, 0xFFE54841, 0xFFE44240, 0xFFE44040 },
new float[] { 0.0f, 0.0807f, 0.1554f, 0.225f, 0.2904f, 0.3526f, 0.4125f, 0.471f, 0.529f, 0.5875f, 0.6474f, 0.7096f, 0.775f, 0.8446f, 0.9193f, 1f },
0f);
public static final GradientChatWallpaper NOIR = new GradientChatWallpaper(180f,
new int[] { 0xFF16161D, 0xFF17171E, 0xFF1A1A22, 0xFF1F1F28, 0xFF26262F, 0xFF2D2D38, 0xFF353542, 0xFF3E3E4C, 0xFF474757, 0xFF4F4F61, 0xFF57576B, 0xFF5F5F74, 0xFF65657C, 0xFF6A6A82, 0xFF6D6D85, 0xFF6E6E87 },
new float[] { 0.0000f, 0.0807f, 0.1554f, 0.2250f, 0.2904f, 0.3526f, 0.4125f, 0.4710f, 0.5290f, 0.5875f, 0.6474f, 0.7096f, 0.7750f, 0.8446f, 0.9193f, 1.0000f },
0f);
public static final GradientChatWallpaper HEATMAP = new GradientChatWallpaper(192f,
new int[] { 0xFFF53844, 0xFFF33845, 0xFFEC3848, 0xFFE2384C, 0xFFD63851, 0xFFC73857, 0xFFB6385E, 0xFFA43866, 0xFF93376D, 0xFF813775, 0xFF70377C, 0xFF613782, 0xFF553787, 0xFF4B378B, 0xFF44378E, 0xFF42378F },
new float[] { 0.0000f, 0.0075f, 0.0292f, 0.0637f, 0.1097f, 0.1659f, 0.2310f, 0.3037f, 0.3827f, 0.4666f, 0.5541f, 0.6439f, 0.7347f, 0.8252f, 0.9141f, 1.0000f },
0f);
public static final GradientChatWallpaper AQUA = new GradientChatWallpaper(180f,
new int[] { 0xFF0093E9, 0xFF0294E9, 0xFF0696E7, 0xFF0D99E5, 0xFF169EE3, 0xFF21A3E0, 0xFF2DA8DD, 0xFF3AAEDA, 0xFF46B5D6, 0xFF53BBD3, 0xFF5FC0D0, 0xFF6AC5CD, 0xFF73CACB, 0xFF7ACDC9, 0xFF7ECFC7, 0xFF80D0C7 },
new float[] { 0.0000f, 0.0807f, 0.1554f, 0.2250f, 0.2904f, 0.3526f, 0.4125f, 0.4710f, 0.5290f, 0.5875f, 0.6474f, 0.7096f, 0.7750f, 0.8446f, 0.9193f, 1.0000f },
0f);
public static final GradientChatWallpaper IRIDESCENT = new GradientChatWallpaper(192f,
new int[] { 0xFFF04CE6, 0xFFEE4BE6, 0xFFE54AE5, 0xFFD949E5, 0xFFC946E4, 0xFFB644E3, 0xFFA141E3, 0xFF8B3FE2, 0xFF743CE1, 0xFF5E39E0, 0xFF4936DF, 0xFF3634DE, 0xFF2632DD, 0xFF1930DD, 0xFF112FDD, 0xFF0E2FDD },
new float[] { 0.0000f, 0.0807f, 0.1554f, 0.2250f, 0.2904f, 0.3526f, 0.4125f, 0.4710f, 0.5290f, 0.5875f, 0.6474f, 0.7096f, 0.7750f, 0.8446f, 0.9193f, 1.0000f },
0f);
public static final GradientChatWallpaper MONSTERA = new GradientChatWallpaper(180f,
new int[] { 0xFF65CDAC, 0xFF64CDAB, 0xFF60CBA8, 0xFF5BC8A3, 0xFF55C49D, 0xFF4DC096, 0xFF45BB8F, 0xFF3CB687, 0xFF33B17F, 0xFF2AAC76, 0xFF21A76F, 0xFF1AA268, 0xFF139F62, 0xFF0E9C5E, 0xFF0B9A5B, 0xFF0A995A },
new float[] { 0.0000f, 0.0807f, 0.1554f, 0.2250f, 0.2904f, 0.3526f, 0.4125f, 0.4710f, 0.5290f, 0.5875f, 0.6474f, 0.7096f, 0.7750f, 0.8446f, 0.9193f, 1.0000f },
0f);
public static final GradientChatWallpaper BLISS = new GradientChatWallpaper(180f,
new int[] { 0xFFD8E1FA, 0xFFD8E0F9, 0xFFD8DEF7, 0xFFD8DBF3, 0xFFD8D6EE, 0xFFD7D1E8, 0xFFD7CCE2, 0xFFD7C6DB, 0xFFD7BFD4, 0xFFD7B9CD, 0xFFD6B4C7, 0xFFD6AFC1, 0xFFD6AABC, 0xFFD6A7B8, 0xFFD6A5B6, 0xFFD6A4B5 },
new float[] { 0.0000f, 0.0807f, 0.1554f, 0.2250f, 0.2904f, 0.3526f, 0.4125f, 0.4710f, 0.5290f, 0.5875f, 0.6474f, 0.7096f, 0.7750f, 0.8446f, 0.9193f, 1.0000f },
0f);
public static final GradientChatWallpaper SKY = new GradientChatWallpaper(180f,
new int[] { 0xFFD8EBFD, 0xFFD7EAFD, 0xFFD5E9FD, 0xFFD2E7FD, 0xFFCDE5FD, 0xFFC8E3FD, 0xFFC3E0FD, 0xFFBDDDFC, 0xFFB7DAFC, 0xFFB2D7FC, 0xFFACD4FC, 0xFFA7D1FC, 0xFFA3CFFB, 0xFFA0CDFB, 0xFF9ECCFB, 0xFF9DCCFB },
new float[] { 0.0000f, 0.0807f, 0.1554f, 0.2250f, 0.2904f, 0.3526f, 0.4125f, 0.4710f, 0.5290f, 0.5875f, 0.6474f, 0.7096f, 0.7750f, 0.8446f, 0.9193f, 1.0000f },
0f);
public static final GradientChatWallpaper PEACH = new GradientChatWallpaper(192f,
new int[] { 0xFFFFE5C2, 0xFFFFE4C1, 0xFFFFE2BF, 0xFFFFDFBD, 0xFFFEDBB9, 0xFFFED6B5, 0xFFFED1B1, 0xFFFDCCAC, 0xFFFDC6A8, 0xFFFDC0A3, 0xFFFCBB9F, 0xFFFCB69B, 0xFFFCB297, 0xFFFCAF95, 0xFFFCAD93, 0xFFFCAC92 },
new float[] { 0.0000f, 0.0807f, 0.1554f, 0.2250f, 0.2904f, 0.3526f, 0.4125f, 0.4710f, 0.5290f, 0.5875f, 0.6474f, 0.7096f, 0.7750f, 0.8446f, 0.9193f, 1.0000f },
0f);
private final float degrees;
@@ -155,6 +155,10 @@ public final class GradientChatWallpaper implements ChatWallpaper, Parcelable {
return result;
}
public int[] getColors() {
return colors;
}
public static final Creator<GradientChatWallpaper> CREATOR = new Creator<GradientChatWallpaper>() {
@Override
public GradientChatWallpaper createFromParcel(Parcel in) {

View File

@@ -14,18 +14,18 @@ import java.util.Objects;
public final class SingleColorChatWallpaper implements ChatWallpaper, Parcelable {
public static final ChatWallpaper BLUSH = new SingleColorChatWallpaper(0xFFE26983, 0f);
public static final ChatWallpaper COPPER = new SingleColorChatWallpaper(0xFFDF9171, 0f);
public static final ChatWallpaper DUST = new SingleColorChatWallpaper(0xFF9E9887, 0f);
public static final ChatWallpaper CELADON = new SingleColorChatWallpaper(0xFF89AE8F, 0f);
public static final ChatWallpaper RAINFOREST = new SingleColorChatWallpaper(0xFF146148, 0f);
public static final ChatWallpaper PACIFIC = new SingleColorChatWallpaper(0xFF32C7E2, 0f);
public static final ChatWallpaper FROST = new SingleColorChatWallpaper(0xFF7C99B6, 0f);
public static final ChatWallpaper NAVY = new SingleColorChatWallpaper(0xFF403B91, 0f);
public static final ChatWallpaper LILAC = new SingleColorChatWallpaper(0xFFC988E7, 0f);
public static final ChatWallpaper PINK = new SingleColorChatWallpaper(0xFFE297C3, 0f);
public static final ChatWallpaper EGGPLANT = new SingleColorChatWallpaper(0xFF624249, 0f);
public static final ChatWallpaper SILVER = new SingleColorChatWallpaper(0xFFA2A2AA, 0f);
public static final SingleColorChatWallpaper BLUSH = new SingleColorChatWallpaper(0xFFE26983, 0f);
public static final SingleColorChatWallpaper COPPER = new SingleColorChatWallpaper(0xFFDF9171, 0f);
public static final SingleColorChatWallpaper DUST = new SingleColorChatWallpaper(0xFF9E9887, 0f);
public static final SingleColorChatWallpaper CELADON = new SingleColorChatWallpaper(0xFF89AE8F, 0f);
public static final SingleColorChatWallpaper RAINFOREST = new SingleColorChatWallpaper(0xFF146148, 0f);
public static final SingleColorChatWallpaper PACIFIC = new SingleColorChatWallpaper(0xFF32C7E2, 0f);
public static final SingleColorChatWallpaper FROST = new SingleColorChatWallpaper(0xFF7C99B6, 0f);
public static final SingleColorChatWallpaper NAVY = new SingleColorChatWallpaper(0xFF403B91, 0f);
public static final SingleColorChatWallpaper LILAC = new SingleColorChatWallpaper(0xFFC988E7, 0f);
public static final SingleColorChatWallpaper PINK = new SingleColorChatWallpaper(0xFFE297C3, 0f);
public static final SingleColorChatWallpaper EGGPLANT = new SingleColorChatWallpaper(0xFF624249, 0f);
public static final SingleColorChatWallpaper SILVER = new SingleColorChatWallpaper(0xFFA2A2AA, 0f);
private final @ColorInt int color;
private final float dimLevelInDarkTheme;
@@ -95,6 +95,10 @@ public final class SingleColorChatWallpaper implements ChatWallpaper, Parcelable
return Objects.hash(color, dimLevelInDarkTheme);
}
public int getColor() {
return color;
}
public static final Creator<SingleColorChatWallpaper> CREATOR = new Creator<SingleColorChatWallpaper>() {
@Override
public SingleColorChatWallpaper createFromParcel(Parcel in) {

View File

@@ -27,7 +27,7 @@ import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
final class UriChatWallpaper implements ChatWallpaper, Parcelable {
public final class UriChatWallpaper implements ChatWallpaper, Parcelable {
private static final LruCache<Uri, Bitmap> CACHE = new LruCache<Uri, Bitmap>((int) Runtime.getRuntime().maxMemory() / 8) {
@Override
@@ -118,6 +118,10 @@ final class UriChatWallpaper implements ChatWallpaper, Parcelable {
return false;
}
public @NonNull Uri getUri() {
return uri;
}
@Override
public @NonNull Wallpaper serialize() {
return new Wallpaper.Builder()

View File

@@ -1,18 +1,19 @@
package org.thoughtcrime.securesms.wallpaper;
import android.content.Context;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.WorkerThread;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.attachments.AttachmentId;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.jobs.UploadAttachmentToArchiveJob;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.util.storage.FileStorage;
import org.thoughtcrime.securesms.mms.PartUriParser;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
@@ -26,31 +27,28 @@ public final class WallpaperStorage {
private static final String TAG = Log.tag(WallpaperStorage.class);
private static final String DIRECTORY = "wallpapers";
private static final String FILENAME_BASE = "wallpaper";
/**
* Saves the provided input stream as a new wallpaper file.
*/
@WorkerThread
public static @NonNull ChatWallpaper save(@NonNull Context context, @NonNull InputStream wallpaperStream, @NonNull String extension) throws IOException {
String name = FileStorage.save(context, wallpaperStream, DIRECTORY, FILENAME_BASE, extension);
public static @NonNull ChatWallpaper save(@NonNull InputStream wallpaperStream) throws IOException {
AttachmentId attachmentId = SignalDatabase.attachments().insertWallpaper(wallpaperStream);
return ChatWallpaperFactory.create(PartAuthority.getWallpaperUri(name));
if (SignalStore.backup().backsUpMedia()) {
AppDependencies.getJobManager().add(new UploadAttachmentToArchiveJob(attachmentId));
}
return ChatWallpaperFactory.create(PartAuthority.getAttachmentDataUri(attachmentId));
}
@WorkerThread
public static @NonNull InputStream read(@NonNull Context context, String filename) throws IOException {
return FileStorage.read(context, DIRECTORY, filename);
}
@WorkerThread
public static @NonNull List<ChatWallpaper> getAll(@NonNull Context context) {
return FileStorage.getAll(context, DIRECTORY, FILENAME_BASE)
.stream()
.map(PartAuthority::getWallpaperUri)
.map(ChatWallpaperFactory::create)
.collect(Collectors.toList());
public static @NonNull List<ChatWallpaper> getAll() {
return SignalDatabase.attachments()
.getAllWallpapers()
.stream()
.map(PartAuthority::getAttachmentDataUri)
.map(ChatWallpaperFactory::create)
.collect(Collectors.toList());
}
/**
@@ -58,7 +56,7 @@ public final class WallpaperStorage {
* if we discover it's unused, we'll delete the file.
*/
@WorkerThread
public static void onWallpaperDeselected(@NonNull Context context, @NonNull Uri uri) {
public static void onWallpaperDeselected(@NonNull Uri uri) {
Uri globalUri = SignalStore.wallpaper().getWallpaperUri();
if (Objects.equals(uri, globalUri)) {
return;
@@ -69,12 +67,7 @@ public final class WallpaperStorage {
return;
}
String filename = PartAuthority.getWallpaperFilename(uri);
File directory = context.getDir(DIRECTORY, Context.MODE_PRIVATE);
File wallpaperFile = new File(directory, filename);
if (!wallpaperFile.delete()) {
Log.w(TAG, "Failed to delete " + filename + "!");
}
AttachmentId attachmentId = new PartUriParser(uri).getPartId();
SignalDatabase.attachments().deleteAttachment(attachmentId);
}
}

View File

@@ -33,14 +33,14 @@ final class WallpaperCropRepository {
@WorkerThread
@NonNull ChatWallpaper setWallPaper(byte[] bytes) throws IOException {
try (InputStream inputStream = new ByteArrayInputStream(bytes)) {
ChatWallpaper wallpaper = WallpaperStorage.save(context, inputStream, "webp");
ChatWallpaper wallpaper = WallpaperStorage.save(inputStream);
if (recipientId != null) {
Log.i(TAG, "Setting image wallpaper for " + recipientId);
SignalDatabase.recipients().setWallpaper(recipientId, wallpaper);
SignalDatabase.recipients().setWallpaper(recipientId, wallpaper, true);
} else {
Log.i(TAG, "Setting image wallpaper for default");
SignalStore.wallpaper().setWallpaper(context, wallpaper);
SignalStore.wallpaper().setWallpaper(wallpaper);
}
return wallpaper;

View File

@@ -621,7 +621,7 @@ message FilePointer {
oneof locator {
BackupLocator backupLocator = 1;
AttachmentLocator attachmentLocator= 2;
AttachmentLocator attachmentLocator = 2;
InvalidAttachmentLocator invalidAttachmentLocator = 3;
}

View File

@@ -1,3 +1,8 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.util;
import android.app.Application;
@@ -10,7 +15,6 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.ParameterizedRobolectricTestRunner;
import org.robolectric.annotation.Config;
import org.thoughtcrime.securesms.BuildConfig;
import java.util.Arrays;
import java.util.Collection;
@@ -24,7 +28,7 @@ public class UriUtilTest_isValidExternalUri {
private final String input;
private final boolean output;
private static final String APPLICATION_ID = BuildConfig.APPLICATION_ID;
private static final String APPLICATION_ID = "org.thoughtcrime.securesms";
@ParameterizedRobolectricTestRunner.Parameters
public static Collection<Object[]> data() {

View File

@@ -39,7 +39,8 @@ tasks.withType<KotlinCompile>().configureEach {
afterEvaluate {
listOf(
"runKtlintCheckOverMainSourceSet",
"runKtlintFormatOverMainSourceSet"
"runKtlintFormatOverMainSourceSet",
"sourcesJar"
).forEach { taskName ->
tasks.named(taskName) {
mustRunAfter(tasks.named("generateMainProtos"))