mirror of
https://github.com/mollyim/mollyim-insider-android.git
synced 2025-05-12 21:30:40 +01:00
Merge tag 'v7.2.4' into molly-7.2
This commit is contained in:
commit
1d4fea6aad
270 changed files with 12321 additions and 2475 deletions
|
@ -19,8 +19,8 @@ apply {
|
|||
from("fix-profm.gradle")
|
||||
}
|
||||
|
||||
val canonicalVersionCode = 1400
|
||||
val canonicalVersionName = "7.1.3"
|
||||
val canonicalVersionCode = 1405
|
||||
val canonicalVersionName = "7.2.4"
|
||||
val mollyRevision = 1
|
||||
|
||||
val postFixSize = 100
|
||||
|
@ -189,7 +189,7 @@ android {
|
|||
buildConfigField("String", "CDSI_MRENCLAVE", "\"0f6fd79cdfdaa5b2e6337f534d3baf999318b0c462a7ac1f41297a3e4b424a57\"")
|
||||
buildConfigField("String", "SVR2_MRENCLAVE", "\"a6622ad4656e1abcd0bc0ff17c229477747d2ded0495c4ebee7ed35c1789fa97\"")
|
||||
buildConfigField("String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF\"")
|
||||
buildConfigField("String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"AMhf5ywVwITZMsff/eCyudZx9JDmkkkbV6PInzG4p8x3VqVJSFiMvnvlEKWuRob/1eaIetR31IYeAbm0NdOuHH8Qi+Rexi1wLlpzIo1gstHWBfZzy1+qHRV5A4TqPp15YzBPm0WSggW6PbSn+F4lf57VCnHF7p8SvzAA2ZZJPYJURt8X7bbg+H3i+PEjH9DXItNEqs2sNcug37xZQDLm7X36nOoGPs54XsEGzPdEV+itQNGUFEjY6X9Uv+Acuks7NpyGvCoKxGwgKgE5XyJ+nNKlyHHOLb6N1NuHyBrZrgtY/JYJHRooo5CEqYKBqdFnmbTVGEkCvJKxLnjwKWf+fEPoWeQFj5ObDjcKMZf2Jm2Ae69x+ikU5gBXsRmoF94GXTLfN0/vLt98KDPnxwAQL9j5V1jGOY8jQl6MLxEs56cwXN0dqCnImzVH3TZT1cJ8SW1BRX6qIVxEzjsSGx3yxF3suAilPMqGRp4ffyopjMD1JXiKR2RwLKzizUe5e8XyGOy9fplzhw3jVzTRyUZTRSZKkMLWcQ/gv0E4aONNqs4P+NameAZYOD12qRkxosQQP5uux6B2nRyZ7sAV54DgFyLiRcq1FvwKw2EPQdk4HDoePrO/RNUbyNddnM/mMgj4FW65xCoT1LmjrIjsv/Ggdlx46ueczhMgtBunx1/w8k8V+l8LVZ8gAT6wkU5J+DPQalQguMg12Jzug3q4TbdHiGCmD9EunCwOmsLuLJkz6EcSYXtrlDEnAM+hicw7iergYLLlMXpfTdGxJCWJmP4zqUFeTTmsmhsjGBt7NiEB/9pFFEB3pSbf4iiUukw63Eo8Aqnf4iwob6X1QviCWuc8t0I=\"")
|
||||
buildConfigField("String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"AMhf5ywVwITZMsff/eCyudZx9JDmkkkbV6PInzG4p8x3VqVJSFiMvnvlEKWuRob/1eaIetR31IYeAbm0NdOuHH8Qi+Rexi1wLlpzIo1gstHWBfZzy1+qHRV5A4TqPp15YzBPm0WSggW6PbSn+F4lf57VCnHF7p8SvzAA2ZZJPYJURt8X7bbg+H3i+PEjH9DXItNEqs2sNcug37xZQDLm7X36nOoGPs54XsEGzPdEV+itQNGUFEjY6X9Uv+Acuks7NpyGvCoKxGwgKgE5XyJ+nNKlyHHOLb6N1NuHyBrZrgtY/JYJHRooo5CEqYKBqdFnmbTVGEkCvJKxLnjwKWf+fEPoWeQFj5ObDjcKMZf2Jm2Ae69x+ikU5gBXsRmoF94GXTLfN0/vLt98KDPnxwAQL9j5V1jGOY8jQl6MLxEs56cwXN0dqCnImzVH3TZT1cJ8SW1BRX6qIVxEzjsSGx3yxF3suAilPMqGRp4ffyopjMD1JXiKR2RwLKzizUe5e8XyGOy9fplzhw3jVzTRyUZTRSZKkMLWcQ/gv0E4aONNqs4P+NameAZYOD12qRkxosQQP5uux6B2nRyZ7sAV54DgFyLiRcq1FvwKw2EPQdk4HDoePrO/RNUbyNddnM/mMgj4FW65xCoT1LmjrIjsv/Ggdlx46ueczhMgtBunx1/w8k8V+l8LVZ8gAT6wkU5J+DPQalQguMg12Jzug3q4TbdHiGCmD9EunCwOmsLuLJkz6EcSYXtrlDEnAM+hicw7iergYLLlMXpfTdGxJCWJmP4zqUFeTTmsmhsjGBt7NiEB/9pFFEB3pSbf4iiUukw63Eo8Aqnf4iwob6X1QviCWuc8t0LUlT9vALgh/f2DPVOOmR0RW6bgRvc7DSF20V/omg+YBw==\"")
|
||||
buildConfigField("String", "GENERIC_SERVER_PUBLIC_PARAMS", "\"AByD873dTilmOSG0TjKrvpeaKEsUmIO8Vx9BeMmftwUs9v7ikPwM8P3OHyT0+X3EUMZrSe9VUp26Wai51Q9I8mdk0hX/yo7CeFGJyzoOqn8e/i4Ygbn5HoAyXJx5eXfIbqpc0bIxzju4H/HOQeOpt6h742qii5u/cbwOhFZCsMIbElZTaeU+BWMBQiZHIGHT5IE0qCordQKZ5iPZom0HeFa8Yq0ShuEyAl0WINBiY6xE3H/9WnvzXBbMuuk//eRxXgzO8ieCeK8FwQNxbfXqZm6Ro1cMhCOF3u7xoX83QhpN\"")
|
||||
buildConfigField("String", "BACKUP_SERVER_PUBLIC_PARAMS", "\"AJwNSU55fsFCbgaxGRD11wO1juAs8Yr5GF8FPlGzzvdJJIKH5/4CC7ZJSOe3yL2vturVaRU2Cx0n751Vt8wkj1bozK3CBV1UokxV09GWf+hdVImLGjXGYLLhnI1J2TWEe7iWHyb553EEnRb5oxr9n3lUbNAJuRmFM7hrr0Al0F0wrDD4S8lo2mGaXe0MJCOM166F8oYRQqpFeEHfiLnxA1O8ZLh7vMdv4g9jI5phpRBTsJ5IjiJrWeP0zdIGHEssUeprDZ9OUJ14m0v61eYJMKsf59Bn+mAT2a7YfB+Don9O\"")
|
||||
buildConfigField("String[]", "LANGUAGES", "new String[]{ ${languageList().map { "\"$it\"" }.joinToString(separator = ", ")} }")
|
||||
|
@ -202,6 +202,7 @@ android {
|
|||
// MOLLY: Rely on the built-in variables FLAVOR and BUILD_TYPE instead of BUILD_*_TYPE
|
||||
buildConfigField("String", "BADGE_STATIC_ROOT", "\"https://updates2.signal.org/static/badges/\"")
|
||||
buildConfigField("boolean", "TRACING_ENABLED", "false")
|
||||
buildConfigField("boolean", "MESSAGE_BACKUP_RESTORE_ENABLED", "false")
|
||||
|
||||
ndk {
|
||||
//noinspection ChromeOsAbiSupport
|
||||
|
@ -335,7 +336,7 @@ android {
|
|||
buildConfigField("String", "SIGNAL_SVR2_URL", "\"https://svr2.staging.signal.org\"")
|
||||
buildConfigField("String", "SVR2_MRENCLAVE", "\"acb1973aa0bbbd14b3b4e06f145497d948fd4a98efc500fcce363b3b743ec482\"")
|
||||
buildConfigField("String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx\"")
|
||||
buildConfigField("String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"ABSY21VckQcbSXVNCGRYJcfWHiAMZmpTtTELcDmxgdFbtp/bWsSxZdMKzfCp8rvIs8ocCU3B37fT3r4Mi5qAemeGeR2X+/YmOGR5ofui7tD5mDQfstAI9i+4WpMtIe8KC3wU5w3Inq3uNWVmoGtpKndsNfwJrCg0Hd9zmObhypUnSkfYn2ooMOOnBpfdanRtrvetZUayDMSC5iSRcXKpdlukrpzzsCIvEwjwQlJYVPOQPj4V0F4UXXBdHSLK05uoPBCQG8G9rYIGedYsClJXnbrgGYG3eMTG5hnx4X4ntARBgELuMWWUEEfSK0mjXg+/2lPmWcTZWR9nkqgQQP0tbzuiPm74H2wMO4u1Wafe+UwyIlIT9L7KLS19Aw8r4sPrXZSSsOZ6s7M1+rTJN0bI5CKY2PX29y5Ok3jSWufIKcgKOnWoP67d5b2du2ZVJjpjfibNIHbT/cegy/sBLoFwtHogVYUewANUAXIaMPyCLRArsKhfJ5wBtTminG/PAvuBdJ70Z/bXVPf8TVsR292zQ65xwvWTejROW6AZX6aqucUjlENAErBme1YHmOSpU6tr6doJ66dPzVAWIanmO/5mgjNEDeK7DDqQdB1xd03HT2Qs2TxY3kCK8aAb/0iM0HQiXjxZ9HIgYhbtvGEnDKW5ILSUydqH/KBhW4Pb0jZWnqN/YgbWDKeJxnDbYcUob5ZY5Lt5ZCMKuaGUvCJRrCtuugSMaqjowCGRempsDdJEt+cMaalhZ6gczklJB/IbdwENW9KeVFPoFNFzhxWUIS5ML9riVYhAtE6JE5jX0xiHNVIIPthb458cfA8daR0nYfYAUKogQArm0iBezOO+mPk5vCM=\"")
|
||||
buildConfigField("String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"ABSY21VckQcbSXVNCGRYJcfWHiAMZmpTtTELcDmxgdFbtp/bWsSxZdMKzfCp8rvIs8ocCU3B37fT3r4Mi5qAemeGeR2X+/YmOGR5ofui7tD5mDQfstAI9i+4WpMtIe8KC3wU5w3Inq3uNWVmoGtpKndsNfwJrCg0Hd9zmObhypUnSkfYn2ooMOOnBpfdanRtrvetZUayDMSC5iSRcXKpdlukrpzzsCIvEwjwQlJYVPOQPj4V0F4UXXBdHSLK05uoPBCQG8G9rYIGedYsClJXnbrgGYG3eMTG5hnx4X4ntARBgELuMWWUEEfSK0mjXg+/2lPmWcTZWR9nkqgQQP0tbzuiPm74H2wMO4u1Wafe+UwyIlIT9L7KLS19Aw8r4sPrXZSSsOZ6s7M1+rTJN0bI5CKY2PX29y5Ok3jSWufIKcgKOnWoP67d5b2du2ZVJjpjfibNIHbT/cegy/sBLoFwtHogVYUewANUAXIaMPyCLRArsKhfJ5wBtTminG/PAvuBdJ70Z/bXVPf8TVsR292zQ65xwvWTejROW6AZX6aqucUjlENAErBme1YHmOSpU6tr6doJ66dPzVAWIanmO/5mgjNEDeK7DDqQdB1xd03HT2Qs2TxY3kCK8aAb/0iM0HQiXjxZ9HIgYhbtvGEnDKW5ILSUydqH/KBhW4Pb0jZWnqN/YgbWDKeJxnDbYcUob5ZY5Lt5ZCMKuaGUvCJRrCtuugSMaqjowCGRempsDdJEt+cMaalhZ6gczklJB/IbdwENW9KeVFPoFNFzhxWUIS5ML9riVYhAtE6JE5jX0xiHNVIIPthb458cfA8daR0nYfYAUKogQArm0iBezOO+mPk5vCNWI+wwkyFCqNDXz/qxl1gAntuCJtSfq9OC3NkdhQlgYQ==\"")
|
||||
buildConfigField("String", "GENERIC_SERVER_PUBLIC_PARAMS", "\"AHILOIrFPXX9laLbalbA9+L1CXpSbM/bTJXZGZiuyK1JaI6dK5FHHWL6tWxmHKYAZTSYmElmJ5z2A5YcirjO/yfoemE03FItyaf8W1fE4p14hzb5qnrmfXUSiAIVrhaXVwIwSzH6RL/+EO8jFIjJ/YfExfJ8aBl48CKHgu1+A6kWynhttonvWWx6h7924mIzW0Czj2ROuh4LwQyZypex4GuOPW8sgIT21KNZaafgg+KbV7XM1x1tF3XA17B4uGUaDbDw2O+nR1+U5p6qHPzmJ7ggFjSN6Utu+35dS1sS0P9N\"")
|
||||
buildConfigField("String", "BACKUP_SERVER_PUBLIC_PARAMS", "\"AHYrGb9IfugAAJiPKp+mdXUx+OL9zBolPYHYQz6GI1gWjpEu5me3zVNSvmYY4zWboZHif+HG1sDHSuvwFd0QszSwuSF4X4kRP3fJREdTZ5MCR0n55zUppTwfHRW2S4sdQ0JGz7YDQIJCufYSKh0pGNEHL6hv79Agrdnr4momr3oXdnkpVBIp3HWAQ6IbXQVSG18X36GaicI1vdT0UFmTwU2KTneluC2eyL9c5ff8PcmiS+YcLzh0OKYQXB5ZfQ06d6DiINvDQLy75zcfUOniLAj0lGJiHxGczin/RXisKSR8\"")
|
||||
buildConfigField("String", "MOBILE_COIN_ENVIRONMENT", "\"testnet\"")
|
||||
|
@ -345,6 +346,7 @@ android {
|
|||
|
||||
buildConfigField("String", "BUILD_ENVIRONMENT_TYPE", "\"Staging\"")
|
||||
buildConfigField("String", "STRIPE_PUBLISHABLE_KEY", "\"pk_test_sngOd8FnXNkpce9nPXawKrJD00kIDngZkD\"")
|
||||
buildConfigField("boolean", "MESSAGE_BACKUP_RESTORE_ENABLED", "true")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -443,6 +445,7 @@ dependencies {
|
|||
implementation(libs.androidx.activity.compose)
|
||||
implementation(libs.androidx.camera.core)
|
||||
implementation(libs.androidx.camera.camera2)
|
||||
implementation(libs.androidx.camera.extensions)
|
||||
implementation(libs.androidx.camera.lifecycle)
|
||||
implementation(libs.androidx.camera.view)
|
||||
implementation(libs.androidx.concurrent.futures)
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
-dontobfuscate
|
||||
-keepattributes SourceFile,LineNumberTable
|
||||
-keep class org.whispersystems.** { *; }
|
||||
-keep class org.signal.libsignal.net.** { *; }
|
||||
-keep class org.signal.libsignal.protocol.** { *; }
|
||||
-keep class org.signal.libsignal.usernames.** { *; }
|
||||
-keep class org.thoughtcrime.securesms.** { *; }
|
||||
-keep class org.signal.donations.json.** { *; }
|
||||
-keepclassmembers class ** {
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -51,18 +51,16 @@ class AttachmentTableTest {
|
|||
|
||||
SignalDatabase.attachments.updateAttachmentData(
|
||||
attachment,
|
||||
createMediaStream(byteArrayOf(1, 2, 3, 4, 5)),
|
||||
false
|
||||
createMediaStream(byteArrayOf(1, 2, 3, 4, 5))
|
||||
)
|
||||
|
||||
SignalDatabase.attachments.updateAttachmentData(
|
||||
attachment2,
|
||||
createMediaStream(byteArrayOf(1, 2, 3)),
|
||||
false
|
||||
createMediaStream(byteArrayOf(1, 2, 3))
|
||||
)
|
||||
|
||||
val attachment1Info = SignalDatabase.attachments.getAttachmentDataFileInfo(attachment.attachmentId, AttachmentTable.DATA_FILE)
|
||||
val attachment2Info = SignalDatabase.attachments.getAttachmentDataFileInfo(attachment2.attachmentId, AttachmentTable.DATA_FILE)
|
||||
val attachment1Info = SignalDatabase.attachments.getDataFileInfo(attachment.attachmentId)
|
||||
val attachment2Info = SignalDatabase.attachments.getDataFileInfo(attachment2.attachmentId)
|
||||
|
||||
assertNotEquals(attachment1Info, attachment2Info)
|
||||
}
|
||||
|
@ -79,18 +77,16 @@ class AttachmentTableTest {
|
|||
|
||||
SignalDatabase.attachments.updateAttachmentData(
|
||||
attachment,
|
||||
createMediaStream(byteArrayOf(1, 2, 3, 4, 5)),
|
||||
true
|
||||
createMediaStream(byteArrayOf(1, 2, 3, 4, 5))
|
||||
)
|
||||
|
||||
SignalDatabase.attachments.updateAttachmentData(
|
||||
attachment2,
|
||||
createMediaStream(byteArrayOf(1, 2, 3, 4)),
|
||||
true
|
||||
createMediaStream(byteArrayOf(1, 2, 3, 4))
|
||||
)
|
||||
|
||||
val attachment1Info = SignalDatabase.attachments.getAttachmentDataFileInfo(attachment.attachmentId, AttachmentTable.DATA_FILE)
|
||||
val attachment2Info = SignalDatabase.attachments.getAttachmentDataFileInfo(attachment2.attachmentId, AttachmentTable.DATA_FILE)
|
||||
val attachment1Info = SignalDatabase.attachments.getDataFileInfo(attachment.attachmentId)
|
||||
val attachment2Info = SignalDatabase.attachments.getDataFileInfo(attachment2.attachmentId)
|
||||
|
||||
assertNotEquals(attachment1Info, attachment2Info)
|
||||
}
|
||||
|
@ -121,15 +117,14 @@ class AttachmentTableTest {
|
|||
val highDatabaseAttachment = SignalDatabase.attachments.insertAttachmentForPreUpload(highQualityPreUpload)
|
||||
|
||||
// WHEN
|
||||
SignalDatabase.attachments.updateAttachmentData(standardDatabaseAttachment, createMediaStream(compressedData), false)
|
||||
SignalDatabase.attachments.updateAttachmentData(standardDatabaseAttachment, createMediaStream(compressedData))
|
||||
|
||||
// THEN
|
||||
val previousInfo = SignalDatabase.attachments.getAttachmentDataFileInfo(previousDatabaseAttachmentId, AttachmentTable.DATA_FILE)!!
|
||||
val standardInfo = SignalDatabase.attachments.getAttachmentDataFileInfo(standardDatabaseAttachment.attachmentId, AttachmentTable.DATA_FILE)!!
|
||||
val highInfo = SignalDatabase.attachments.getAttachmentDataFileInfo(highDatabaseAttachment.attachmentId, AttachmentTable.DATA_FILE)!!
|
||||
val previousInfo = SignalDatabase.attachments.getDataFileInfo(previousDatabaseAttachmentId)!!
|
||||
val standardInfo = SignalDatabase.attachments.getDataFileInfo(standardDatabaseAttachment.attachmentId)!!
|
||||
val highInfo = SignalDatabase.attachments.getDataFileInfo(highDatabaseAttachment.attachmentId)!!
|
||||
|
||||
assertNotEquals(standardInfo, highInfo)
|
||||
standardInfo.file assertIs previousInfo.file
|
||||
highInfo.file assertIsNot standardInfo.file
|
||||
highInfo.file.exists() assertIs true
|
||||
}
|
||||
|
@ -158,9 +153,9 @@ class AttachmentTableTest {
|
|||
val secondHighDatabaseAttachment = SignalDatabase.attachments.insertAttachmentForPreUpload(secondHighQualityPreUpload)
|
||||
|
||||
// THEN
|
||||
val standardInfo = SignalDatabase.attachments.getAttachmentDataFileInfo(standardDatabaseAttachment.attachmentId, AttachmentTable.DATA_FILE)!!
|
||||
val highInfo = SignalDatabase.attachments.getAttachmentDataFileInfo(highDatabaseAttachment.attachmentId, AttachmentTable.DATA_FILE)!!
|
||||
val secondHighInfo = SignalDatabase.attachments.getAttachmentDataFileInfo(secondHighDatabaseAttachment.attachmentId, AttachmentTable.DATA_FILE)!!
|
||||
val standardInfo = SignalDatabase.attachments.getDataFileInfo(standardDatabaseAttachment.attachmentId)!!
|
||||
val highInfo = SignalDatabase.attachments.getDataFileInfo(highDatabaseAttachment.attachmentId)!!
|
||||
val secondHighInfo = SignalDatabase.attachments.getDataFileInfo(secondHighDatabaseAttachment.attachmentId)!!
|
||||
|
||||
highInfo.file assertIsNot standardInfo.file
|
||||
secondHighInfo.file assertIs highInfo.file
|
||||
|
|
|
@ -0,0 +1,804 @@
|
|||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import org.junit.Assert.assertArrayEquals
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNotEquals
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.signal.core.util.Base64
|
||||
import org.signal.core.util.update
|
||||
import org.thoughtcrime.securesms.attachments.AttachmentId
|
||||
import org.thoughtcrime.securesms.attachments.PointerAttachment
|
||||
import org.thoughtcrime.securesms.database.AttachmentTable.TransformProperties
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.mms.MediaStream
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMessage
|
||||
import org.thoughtcrime.securesms.mms.QuoteModel
|
||||
import org.thoughtcrime.securesms.mms.SentMediaQuality
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.MediaUtil
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import java.io.File
|
||||
import java.util.UUID
|
||||
import kotlin.random.Random
|
||||
import kotlin.time.Duration.Companion.days
|
||||
|
||||
/**
|
||||
* Collection of [AttachmentTable] tests focused around deduping logic.
|
||||
*/
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class AttachmentTableTest_deduping {
|
||||
|
||||
companion object {
|
||||
val DATA_A = byteArrayOf(1, 2, 3)
|
||||
val DATA_A_COMPRESSED = byteArrayOf(4, 5, 6)
|
||||
val DATA_A_HASH = byteArrayOf(1, 1, 1)
|
||||
|
||||
val DATA_B = byteArrayOf(7, 8, 9)
|
||||
}
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
SignalStore.account().setAci(ServiceId.ACI.from(UUID.randomUUID()))
|
||||
SignalStore.account().setPni(ServiceId.PNI.from(UUID.randomUUID()))
|
||||
SignalStore.account().setE164("+15558675309")
|
||||
|
||||
SignalDatabase.attachments.deleteAllAttachments()
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates two different files with different data. Should not dedupe.
|
||||
*/
|
||||
@Test
|
||||
fun differentFiles() {
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A)
|
||||
val id2 = insertWithData(DATA_B)
|
||||
|
||||
assertDataFilesAreDifferent(id1, id2)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts files with identical data but with transform properties that make them incompatible. Should not dedupe.
|
||||
*/
|
||||
@Test
|
||||
fun identicalFiles_incompatibleTransforms() {
|
||||
// Non-matching qualities
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A, TransformProperties(sentMediaQuality = SentMediaQuality.STANDARD.code))
|
||||
val id2 = insertWithData(DATA_A, TransformProperties(sentMediaQuality = SentMediaQuality.HIGH.code))
|
||||
|
||||
assertDataFilesAreDifferent(id1, id2)
|
||||
assertDataHashStartMatches(id1, id2)
|
||||
}
|
||||
|
||||
// Non-matching video trim flag
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A, TransformProperties())
|
||||
val id2 = insertWithData(DATA_A, TransformProperties(videoTrim = true))
|
||||
|
||||
assertDataFilesAreDifferent(id1, id2)
|
||||
assertDataHashStartMatches(id1, id2)
|
||||
}
|
||||
|
||||
// Non-matching video trim start time
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A, TransformProperties(videoTrim = true, videoTrimStartTimeUs = 1, videoTrimEndTimeUs = 2))
|
||||
val id2 = insertWithData(DATA_A, TransformProperties(videoTrim = true, videoTrimStartTimeUs = 0, videoTrimEndTimeUs = 2))
|
||||
|
||||
assertDataFilesAreDifferent(id1, id2)
|
||||
assertDataHashStartMatches(id1, id2)
|
||||
}
|
||||
|
||||
// Non-matching video trim end time
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A, TransformProperties(videoTrim = true, videoTrimStartTimeUs = 0, videoTrimEndTimeUs = 1))
|
||||
val id2 = insertWithData(DATA_A, TransformProperties(videoTrim = true, videoTrimStartTimeUs = 0, videoTrimEndTimeUs = 2))
|
||||
|
||||
assertDataFilesAreDifferent(id1, id2)
|
||||
assertDataHashStartMatches(id1, id2)
|
||||
}
|
||||
|
||||
// Non-matching mp4 fast start
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A, TransformProperties(mp4FastStart = true))
|
||||
val id2 = insertWithData(DATA_A, TransformProperties(mp4FastStart = false))
|
||||
|
||||
assertDataFilesAreDifferent(id1, id2)
|
||||
assertDataHashStartMatches(id1, id2)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts files with identical data and compatible transform properties. Should dedupe.
|
||||
*/
|
||||
@Test
|
||||
fun identicalFiles_compatibleTransforms() {
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A)
|
||||
val id2 = insertWithData(DATA_A)
|
||||
|
||||
assertDataFilesAreTheSame(id1, id2)
|
||||
assertDataHashStartMatches(id1, id2)
|
||||
assertSkipTransform(id1, false)
|
||||
assertSkipTransform(id2, false)
|
||||
}
|
||||
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A, TransformProperties(sentMediaQuality = SentMediaQuality.STANDARD.code))
|
||||
val id2 = insertWithData(DATA_A, TransformProperties(sentMediaQuality = SentMediaQuality.STANDARD.code))
|
||||
|
||||
assertDataFilesAreTheSame(id1, id2)
|
||||
assertDataHashStartMatches(id1, id2)
|
||||
assertSkipTransform(id1, false)
|
||||
assertSkipTransform(id2, false)
|
||||
}
|
||||
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A, TransformProperties(sentMediaQuality = SentMediaQuality.HIGH.code))
|
||||
val id2 = insertWithData(DATA_A, TransformProperties(sentMediaQuality = SentMediaQuality.HIGH.code))
|
||||
|
||||
assertDataFilesAreTheSame(id1, id2)
|
||||
assertDataHashStartMatches(id1, id2)
|
||||
assertSkipTransform(id1, false)
|
||||
assertSkipTransform(id2, false)
|
||||
}
|
||||
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A, TransformProperties(videoTrim = true, videoTrimStartTimeUs = 1, videoTrimEndTimeUs = 2))
|
||||
val id2 = insertWithData(DATA_A, TransformProperties(videoTrim = true, videoTrimStartTimeUs = 1, videoTrimEndTimeUs = 2))
|
||||
|
||||
assertDataFilesAreTheSame(id1, id2)
|
||||
assertDataHashStartMatches(id1, id2)
|
||||
assertSkipTransform(id1, false)
|
||||
assertSkipTransform(id2, false)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Walks through various scenarios where files are compressed and uploaded.
|
||||
*/
|
||||
@Test
|
||||
fun compressionAndUploads() {
|
||||
// Matches after the first is compressed, skip transform properly set
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A)
|
||||
compress(id1, DATA_A_COMPRESSED)
|
||||
|
||||
val id2 = insertWithData(DATA_A)
|
||||
|
||||
assertDataFilesAreTheSame(id1, id2)
|
||||
assertDataHashStartMatches(id1, id2)
|
||||
assertSkipTransform(id1, true)
|
||||
assertSkipTransform(id2, true)
|
||||
}
|
||||
|
||||
// Matches after the first is uploaded, skip transform and ending hash properly set
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A)
|
||||
compress(id1, DATA_A_COMPRESSED)
|
||||
upload(id1)
|
||||
|
||||
val id2 = insertWithData(DATA_A)
|
||||
|
||||
assertDataFilesAreTheSame(id1, id2)
|
||||
assertDataHashStartMatches(id1, id2)
|
||||
assertDataHashEndMatches(id1, id2)
|
||||
assertSkipTransform(id1, true)
|
||||
assertSkipTransform(id2, true)
|
||||
}
|
||||
|
||||
// Mimics sending two files at once. Ensures all fields are kept in sync as we compress and upload.
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A)
|
||||
val id2 = insertWithData(DATA_A)
|
||||
|
||||
assertDataFilesAreTheSame(id1, id2)
|
||||
assertDataHashStartMatches(id1, id2)
|
||||
assertSkipTransform(id1, false)
|
||||
assertSkipTransform(id2, false)
|
||||
|
||||
compress(id1, DATA_A_COMPRESSED)
|
||||
|
||||
assertDataFilesAreTheSame(id1, id2)
|
||||
assertDataHashStartMatches(id1, id2)
|
||||
assertSkipTransform(id1, true)
|
||||
assertSkipTransform(id2, true)
|
||||
|
||||
upload(id1)
|
||||
|
||||
assertDataFilesAreTheSame(id1, id2)
|
||||
assertDataHashStartMatches(id1, id2)
|
||||
assertDataHashEndMatches(id1, id2)
|
||||
assertRemoteFieldsMatch(id1, id2)
|
||||
}
|
||||
|
||||
// Re-use the upload when uploaded recently
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A)
|
||||
compress(id1, DATA_A_COMPRESSED)
|
||||
upload(id1, uploadTimestamp = System.currentTimeMillis())
|
||||
|
||||
val id2 = insertWithData(DATA_A)
|
||||
|
||||
assertDataFilesAreTheSame(id1, id2)
|
||||
assertDataHashStartMatches(id1, id2)
|
||||
assertDataHashEndMatches(id1, id2)
|
||||
assertRemoteFieldsMatch(id1, id2)
|
||||
assertSkipTransform(id1, true)
|
||||
assertSkipTransform(id2, true)
|
||||
}
|
||||
|
||||
// Do not re-use old uploads
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A)
|
||||
compress(id1, DATA_A_COMPRESSED)
|
||||
upload(id1, uploadTimestamp = System.currentTimeMillis() - 100.days.inWholeMilliseconds)
|
||||
|
||||
val id2 = insertWithData(DATA_A)
|
||||
|
||||
assertDataFilesAreTheSame(id1, id2)
|
||||
assertDataHashStartMatches(id1, id2)
|
||||
assertDataHashEndMatches(id1, id2)
|
||||
assertSkipTransform(id1, true)
|
||||
assertSkipTransform(id2, true)
|
||||
|
||||
assertDoesNotHaveRemoteFields(id2)
|
||||
}
|
||||
|
||||
// This isn't so much "desirable behavior" as it is documenting how things work.
|
||||
// If an attachment is compressed but not uploaded yet, it will have a DATA_HASH_START that doesn't match the actual file content.
|
||||
// This means that if we insert a new attachment with data that matches the compressed data, we won't find a match.
|
||||
// This is ok because we don't allow forwarding unsent messages, so the chances of the user somehow sending a file that matches data we compressed are very low.
|
||||
// What *is* more common is that the user may send DATA_A again, and in this case we will still catch the dedupe (which is already tested above).
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A)
|
||||
compress(id1, DATA_A_COMPRESSED)
|
||||
|
||||
val id2 = insertWithData(DATA_A_COMPRESSED)
|
||||
|
||||
assertDataFilesAreDifferent(id1, id2)
|
||||
}
|
||||
|
||||
// This represents what would happen if you forward an already-send compressed attachment. We should match, skip transform, and skip upload.
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A)
|
||||
compress(id1, DATA_A_COMPRESSED)
|
||||
upload(id1, uploadTimestamp = System.currentTimeMillis())
|
||||
|
||||
val id2 = insertWithData(DATA_A_COMPRESSED)
|
||||
|
||||
assertDataFilesAreTheSame(id1, id2)
|
||||
assertDataHashEndMatches(id1, id2)
|
||||
assertSkipTransform(id1, true)
|
||||
assertSkipTransform(id1, true)
|
||||
assertRemoteFieldsMatch(id1, id2)
|
||||
}
|
||||
|
||||
// This represents what would happen if you edited a video, sent it, then forwarded it. We should match, skip transform, and skip upload.
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A, TransformProperties(videoTrim = true, videoTrimStartTimeUs = 1, videoTrimEndTimeUs = 2))
|
||||
compress(id1, DATA_A_COMPRESSED)
|
||||
upload(id1, uploadTimestamp = System.currentTimeMillis())
|
||||
|
||||
val id2 = insertWithData(DATA_A_COMPRESSED)
|
||||
|
||||
assertDataFilesAreTheSame(id1, id2)
|
||||
assertDataHashEndMatches(id1, id2)
|
||||
assertSkipTransform(id1, true)
|
||||
assertSkipTransform(id1, true)
|
||||
assertRemoteFieldsMatch(id1, id2)
|
||||
}
|
||||
|
||||
// This represents what would happen if you edited a video, sent it, then forwarded it, but *edited the forwarded video*. We should not dedupe.
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A, TransformProperties(videoTrim = true, videoTrimStartTimeUs = 1, videoTrimEndTimeUs = 2))
|
||||
compress(id1, DATA_A_COMPRESSED)
|
||||
upload(id1, uploadTimestamp = System.currentTimeMillis())
|
||||
|
||||
val id2 = insertWithData(DATA_A_COMPRESSED, TransformProperties(videoTrim = true, videoTrimStartTimeUs = 1, videoTrimEndTimeUs = 2))
|
||||
|
||||
assertDataFilesAreDifferent(id1, id2)
|
||||
assertSkipTransform(id1, true)
|
||||
assertSkipTransform(id2, false)
|
||||
assertDoesNotHaveRemoteFields(id2)
|
||||
}
|
||||
|
||||
// This represents what would happen if you sent an image using standard quality, then forwarded it using high quality.
|
||||
// Since you're forwarding, it doesn't matter if the new thing has a higher quality, we should still match and skip transform.
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A, TransformProperties(sentMediaQuality = SentMediaQuality.STANDARD.code))
|
||||
compress(id1, DATA_A_COMPRESSED)
|
||||
upload(id1, uploadTimestamp = System.currentTimeMillis())
|
||||
|
||||
val id2 = insertWithData(DATA_A_COMPRESSED, TransformProperties(sentMediaQuality = SentMediaQuality.HIGH.code))
|
||||
|
||||
assertDataFilesAreTheSame(id1, id2)
|
||||
assertDataHashEndMatches(id1, id2)
|
||||
assertSkipTransform(id1, true)
|
||||
assertSkipTransform(id1, true)
|
||||
assertRemoteFieldsMatch(id1, id2)
|
||||
}
|
||||
|
||||
// This represents what would happen if you sent an image using high quality, then forwarded it using standard quality.
|
||||
// Since you're forwarding, it doesn't matter if the new thing has a lower quality, we should still match and skip transform.
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A, TransformProperties(sentMediaQuality = SentMediaQuality.HIGH.code))
|
||||
compress(id1, DATA_A_COMPRESSED)
|
||||
upload(id1, uploadTimestamp = System.currentTimeMillis())
|
||||
|
||||
val id2 = insertWithData(DATA_A_COMPRESSED, TransformProperties(sentMediaQuality = SentMediaQuality.STANDARD.code))
|
||||
|
||||
assertDataFilesAreTheSame(id1, id2)
|
||||
assertDataHashEndMatches(id1, id2)
|
||||
assertSkipTransform(id1, true)
|
||||
assertSkipTransform(id1, true)
|
||||
assertRemoteFieldsMatch(id1, id2)
|
||||
}
|
||||
|
||||
// Make sure that files marked as unhashable are all updated together
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A)
|
||||
val id2 = insertWithData(DATA_A)
|
||||
upload(id1)
|
||||
upload(id2)
|
||||
clearHashes(id1)
|
||||
clearHashes(id2)
|
||||
|
||||
val file = dataFile(id1)
|
||||
SignalDatabase.attachments.markDataFileAsUnhashable(file)
|
||||
|
||||
assertDataFilesAreTheSame(id1, id2)
|
||||
assertDataHashEndMatches(id1, id2)
|
||||
|
||||
val dataFileInfo = SignalDatabase.attachments.getDataFileInfo(id1)!!
|
||||
assertTrue(dataFileInfo.hashEnd!!.startsWith("UNHASHABLE-"))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Various deletion scenarios to ensure that duped files don't deleted while there's still references.
|
||||
*/
|
||||
@Test
|
||||
fun deletions() {
|
||||
// Delete original then dupe
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A)
|
||||
val id2 = insertWithData(DATA_A)
|
||||
val dataFile = dataFile(id1)
|
||||
|
||||
assertDataFilesAreTheSame(id1, id2)
|
||||
|
||||
delete(id1)
|
||||
|
||||
assertDeleted(id1)
|
||||
assertRowAndFileExists(id2)
|
||||
assertTrue(dataFile.exists())
|
||||
|
||||
delete(id2)
|
||||
|
||||
assertDeleted(id2)
|
||||
assertFalse(dataFile.exists())
|
||||
}
|
||||
|
||||
// Delete dupe then original
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A)
|
||||
val id2 = insertWithData(DATA_A)
|
||||
val dataFile = dataFile(id1)
|
||||
|
||||
assertDataFilesAreTheSame(id1, id2)
|
||||
|
||||
delete(id2)
|
||||
assertDeleted(id2)
|
||||
assertRowAndFileExists(id1)
|
||||
assertTrue(dataFile.exists())
|
||||
|
||||
delete(id1)
|
||||
assertDeleted(id1)
|
||||
assertFalse(dataFile.exists())
|
||||
}
|
||||
|
||||
// Delete original after it was compressed
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A)
|
||||
compress(id1, DATA_A_COMPRESSED)
|
||||
|
||||
val id2 = insertWithData(DATA_A)
|
||||
|
||||
delete(id1)
|
||||
|
||||
assertDeleted(id1)
|
||||
assertRowAndFileExists(id2)
|
||||
assertSkipTransform(id2, true)
|
||||
}
|
||||
|
||||
// Quotes are weak references and should not prevent us from deleting the file
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A)
|
||||
val id2 = insertQuote(id1)
|
||||
|
||||
val dataFile = dataFile(id1)
|
||||
|
||||
delete(id1)
|
||||
assertDeleted(id1)
|
||||
assertRowExists(id2)
|
||||
assertFalse(dataFile.exists())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun quotes() {
|
||||
// Basic quote deduping
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A)
|
||||
val id2 = insertQuote(id1)
|
||||
|
||||
assertDataFilesAreTheSame(id1, id2)
|
||||
assertDataHashStartMatches(id1, id2)
|
||||
}
|
||||
|
||||
// Making sure remote fields carry
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A)
|
||||
val id2 = insertQuote(id1)
|
||||
upload(id1)
|
||||
|
||||
assertDataFilesAreTheSame(id1, id2)
|
||||
assertDataHashStartMatches(id1, id2)
|
||||
assertDataHashEndMatches(id1, id2)
|
||||
assertRemoteFieldsMatch(id1, id2)
|
||||
}
|
||||
|
||||
// Making sure things work for quotes of videos, which have trickier transform properties
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A, transformProperties = TransformProperties.forVideoTrim(1, 2))
|
||||
compress(id1, DATA_A_COMPRESSED)
|
||||
upload(id1)
|
||||
|
||||
val id2 = insertQuote(id1)
|
||||
|
||||
assertDataFilesAreTheSame(id1, id2)
|
||||
assertDataHashEndMatches(id1, id2)
|
||||
assertRemoteFieldsMatch(id1, id2)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Suite of tests around the migration where we hash all of the attachments and potentially dedupe them.
|
||||
*/
|
||||
@Test
|
||||
fun migration() {
|
||||
// Verifying that getUnhashedDataFile only returns if there's actually missing hashes
|
||||
test {
|
||||
val id = insertWithData(DATA_A)
|
||||
upload(id)
|
||||
assertNull(SignalDatabase.attachments.getUnhashedDataFile())
|
||||
}
|
||||
|
||||
// Verifying that getUnhashedDataFile finds the missing hash
|
||||
test {
|
||||
val id = insertWithData(DATA_A)
|
||||
upload(id)
|
||||
clearHashes(id)
|
||||
assertNotNull(SignalDatabase.attachments.getUnhashedDataFile())
|
||||
}
|
||||
|
||||
// Verifying that getUnhashedDataFile doesn't return if the file isn't done downloading
|
||||
test {
|
||||
val id = insertWithData(DATA_A)
|
||||
upload(id)
|
||||
setTransferState(id, AttachmentTable.TRANSFER_PROGRESS_PENDING)
|
||||
clearHashes(id)
|
||||
assertNull(SignalDatabase.attachments.getUnhashedDataFile())
|
||||
}
|
||||
|
||||
// If two attachments share the same file, when we backfill the hash, make sure both get their hashes set
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A)
|
||||
val id2 = insertWithData(DATA_A)
|
||||
upload(id1)
|
||||
upload(id2)
|
||||
|
||||
clearHashes(id1)
|
||||
clearHashes(id2)
|
||||
|
||||
val file = dataFile(id1)
|
||||
SignalDatabase.attachments.setHashForDataFile(file, DATA_A_HASH)
|
||||
|
||||
assertDataHashEnd(id1, DATA_A_HASH)
|
||||
assertDataHashEndMatches(id1, id2)
|
||||
}
|
||||
|
||||
// Creates a situation where two different attachments have the same data but wrote to different files, and verifies the migration dedupes it
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A)
|
||||
upload(id1)
|
||||
clearHashes(id1)
|
||||
|
||||
val id2 = insertWithData(DATA_A)
|
||||
upload(id2)
|
||||
clearHashes(id2)
|
||||
|
||||
assertDataFilesAreDifferent(id1, id2)
|
||||
|
||||
val file1 = dataFile(id1)
|
||||
SignalDatabase.attachments.setHashForDataFile(file1, DATA_A_HASH)
|
||||
|
||||
assertDataHashEnd(id1, DATA_A_HASH)
|
||||
assertDataFilesAreDifferent(id1, id2)
|
||||
|
||||
val file2 = dataFile(id2)
|
||||
SignalDatabase.attachments.setHashForDataFile(file2, DATA_A_HASH)
|
||||
|
||||
assertDataFilesAreTheSame(id1, id2)
|
||||
assertDataHashEndMatches(id1, id2)
|
||||
assertFalse(file2.exists())
|
||||
}
|
||||
|
||||
// We've got three files now with the same data, with two of them sharing a file. We want to make sure *both* entries that share the same file get deduped.
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A)
|
||||
upload(id1)
|
||||
clearHashes(id1)
|
||||
|
||||
val id2 = insertWithData(DATA_A)
|
||||
val id3 = insertWithData(DATA_A)
|
||||
upload(id2)
|
||||
upload(id3)
|
||||
clearHashes(id2)
|
||||
clearHashes(id3)
|
||||
|
||||
assertDataFilesAreDifferent(id1, id2)
|
||||
assertDataFilesAreTheSame(id2, id3)
|
||||
|
||||
val file1 = dataFile(id1)
|
||||
SignalDatabase.attachments.setHashForDataFile(file1, DATA_A_HASH)
|
||||
assertDataHashEnd(id1, DATA_A_HASH)
|
||||
|
||||
val file2 = dataFile(id2)
|
||||
SignalDatabase.attachments.setHashForDataFile(file2, DATA_A_HASH)
|
||||
|
||||
assertDataFilesAreTheSame(id1, id2)
|
||||
assertDataHashEndMatches(id1, id2)
|
||||
assertDataHashEndMatches(id2, id3)
|
||||
assertFalse(file2.exists())
|
||||
}
|
||||
|
||||
// We don't want to mess with files that are still downloading, so this makes sure that even if data matches, we don't dedupe and don't delete the file
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A)
|
||||
upload(id1)
|
||||
clearHashes(id1)
|
||||
|
||||
val id2 = insertWithData(DATA_A)
|
||||
// *not* uploaded
|
||||
clearHashes(id2)
|
||||
|
||||
assertDataFilesAreDifferent(id1, id2)
|
||||
|
||||
val file1 = dataFile(id1)
|
||||
SignalDatabase.attachments.setHashForDataFile(file1, DATA_A_HASH)
|
||||
assertDataHashEnd(id1, DATA_A_HASH)
|
||||
|
||||
val file2 = dataFile(id2)
|
||||
SignalDatabase.attachments.setHashForDataFile(file2, DATA_A_HASH)
|
||||
|
||||
assertDataFilesAreDifferent(id1, id2)
|
||||
assertTrue(file2.exists())
|
||||
}
|
||||
}
|
||||
|
||||
private class TestContext {
|
||||
fun insertWithData(data: ByteArray, transformProperties: TransformProperties = TransformProperties.empty()): AttachmentId {
|
||||
val uri = BlobProvider.getInstance().forData(data).createForSingleSessionInMemory()
|
||||
|
||||
val attachment = UriAttachmentBuilder.build(
|
||||
id = Random.nextLong(),
|
||||
uri = uri,
|
||||
contentType = MediaUtil.IMAGE_JPEG,
|
||||
transformProperties = transformProperties
|
||||
)
|
||||
|
||||
return SignalDatabase.attachments.insertAttachmentForPreUpload(attachment).attachmentId
|
||||
}
|
||||
|
||||
fun insertQuote(attachmentId: AttachmentId): AttachmentId {
|
||||
val originalAttachment = SignalDatabase.attachments.getAttachment(attachmentId)!!
|
||||
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.self())
|
||||
val messageId = SignalDatabase.messages.insertMessageOutbox(
|
||||
message = OutgoingMessage(
|
||||
threadRecipient = Recipient.self(),
|
||||
sentTimeMillis = System.currentTimeMillis(),
|
||||
body = "some text",
|
||||
outgoingQuote = QuoteModel(
|
||||
id = 123,
|
||||
author = Recipient.self().id,
|
||||
text = "Some quote text",
|
||||
isOriginalMissing = false,
|
||||
attachments = listOf(originalAttachment),
|
||||
mentions = emptyList(),
|
||||
type = QuoteModel.Type.NORMAL,
|
||||
bodyRanges = null
|
||||
)
|
||||
),
|
||||
threadId = threadId,
|
||||
forceSms = false,
|
||||
insertListener = null
|
||||
)
|
||||
|
||||
val attachments = SignalDatabase.attachments.getAttachmentsForMessage(messageId)
|
||||
return attachments[0].attachmentId
|
||||
}
|
||||
|
||||
fun compress(attachmentId: AttachmentId, newData: ByteArray, mp4FastStart: Boolean = false) {
|
||||
val databaseAttachment = SignalDatabase.attachments.getAttachment(attachmentId)!!
|
||||
SignalDatabase.attachments.updateAttachmentData(databaseAttachment, newData.asMediaStream())
|
||||
SignalDatabase.attachments.markAttachmentAsTransformed(attachmentId, withFastStart = mp4FastStart)
|
||||
}
|
||||
|
||||
fun upload(attachmentId: AttachmentId, uploadTimestamp: Long = System.currentTimeMillis()) {
|
||||
SignalDatabase.attachments.finalizeAttachmentAfterUpload(attachmentId, createPointerAttachment(attachmentId, uploadTimestamp), uploadTimestamp)
|
||||
}
|
||||
|
||||
fun delete(attachmentId: AttachmentId) {
|
||||
SignalDatabase.attachments.deleteAttachment(attachmentId)
|
||||
}
|
||||
|
||||
fun dataFile(attachmentId: AttachmentId): File {
|
||||
return SignalDatabase.attachments.getDataFileInfo(attachmentId)!!.file
|
||||
}
|
||||
|
||||
fun setTransferState(attachmentId: AttachmentId, transferState: Int) {
|
||||
// messageId doesn't actually matter -- that's for notifying listeners
|
||||
SignalDatabase.attachments.setTransferState(messageId = -1, attachmentId = attachmentId, transferState = transferState)
|
||||
}
|
||||
|
||||
fun clearHashes(id: AttachmentId) {
|
||||
SignalDatabase.attachments.writableDatabase
|
||||
.update(AttachmentTable.TABLE_NAME)
|
||||
.values(
|
||||
AttachmentTable.DATA_HASH_START to null,
|
||||
AttachmentTable.DATA_HASH_END to null
|
||||
)
|
||||
.where("${AttachmentTable.ID} = ?", id)
|
||||
.run()
|
||||
}
|
||||
|
||||
fun assertDeleted(attachmentId: AttachmentId) {
|
||||
assertNull("$attachmentId exists, but it shouldn't!", SignalDatabase.attachments.getAttachment(attachmentId))
|
||||
}
|
||||
|
||||
fun assertRowAndFileExists(attachmentId: AttachmentId) {
|
||||
val databaseAttachment = SignalDatabase.attachments.getAttachment(attachmentId)
|
||||
assertNotNull("$attachmentId does not exist!", databaseAttachment)
|
||||
|
||||
val dataFileInfo = SignalDatabase.attachments.getDataFileInfo(attachmentId)
|
||||
assertTrue("The file for $attachmentId does not exist!", dataFileInfo!!.file.exists())
|
||||
}
|
||||
|
||||
fun assertRowExists(attachmentId: AttachmentId) {
|
||||
val databaseAttachment = SignalDatabase.attachments.getAttachment(attachmentId)
|
||||
assertNotNull("$attachmentId does not exist!", databaseAttachment)
|
||||
}
|
||||
|
||||
fun assertDataFilesAreTheSame(lhs: AttachmentId, rhs: AttachmentId) {
|
||||
val lhsInfo = SignalDatabase.attachments.getDataFileInfo(lhs)!!
|
||||
val rhsInfo = SignalDatabase.attachments.getDataFileInfo(rhs)!!
|
||||
|
||||
assert(lhsInfo.file.exists())
|
||||
assert(rhsInfo.file.exists())
|
||||
|
||||
assertEquals(lhsInfo.file, rhsInfo.file)
|
||||
assertEquals(lhsInfo.length, rhsInfo.length)
|
||||
assertArrayEquals(lhsInfo.random, rhsInfo.random)
|
||||
}
|
||||
|
||||
fun assertDataFilesAreDifferent(lhs: AttachmentId, rhs: AttachmentId) {
|
||||
val lhsInfo = SignalDatabase.attachments.getDataFileInfo(lhs)!!
|
||||
val rhsInfo = SignalDatabase.attachments.getDataFileInfo(rhs)!!
|
||||
|
||||
assert(lhsInfo.file.exists())
|
||||
assert(rhsInfo.file.exists())
|
||||
|
||||
assertNotEquals(lhsInfo.file, rhsInfo.file)
|
||||
}
|
||||
|
||||
fun assertDataHashStartMatches(lhs: AttachmentId, rhs: AttachmentId) {
|
||||
val lhsInfo = SignalDatabase.attachments.getDataFileInfo(lhs)!!
|
||||
val rhsInfo = SignalDatabase.attachments.getDataFileInfo(rhs)!!
|
||||
|
||||
assertNotNull(lhsInfo.hashStart)
|
||||
assertEquals("DATA_HASH_START's did not match!", lhsInfo.hashStart, rhsInfo.hashStart)
|
||||
}
|
||||
|
||||
fun assertDataHashEndMatches(lhs: AttachmentId, rhs: AttachmentId) {
|
||||
val lhsInfo = SignalDatabase.attachments.getDataFileInfo(lhs)!!
|
||||
val rhsInfo = SignalDatabase.attachments.getDataFileInfo(rhs)!!
|
||||
|
||||
assertNotNull(lhsInfo.hashEnd)
|
||||
assertEquals("DATA_HASH_END's did not match!", lhsInfo.hashEnd, rhsInfo.hashEnd)
|
||||
}
|
||||
|
||||
fun assertDataHashEnd(id: AttachmentId, byteArray: ByteArray) {
|
||||
val dataFileInfo = SignalDatabase.attachments.getDataFileInfo(id)!!
|
||||
assertArrayEquals(byteArray, Base64.decode(dataFileInfo.hashEnd!!))
|
||||
}
|
||||
|
||||
fun assertRemoteFieldsMatch(lhs: AttachmentId, rhs: AttachmentId) {
|
||||
val lhsAttachment = SignalDatabase.attachments.getAttachment(lhs)!!
|
||||
val rhsAttachment = SignalDatabase.attachments.getAttachment(rhs)!!
|
||||
|
||||
assertEquals(lhsAttachment.remoteLocation, rhsAttachment.remoteLocation)
|
||||
assertEquals(lhsAttachment.remoteKey, rhsAttachment.remoteKey)
|
||||
assertArrayEquals(lhsAttachment.remoteDigest, rhsAttachment.remoteDigest)
|
||||
assertArrayEquals(lhsAttachment.incrementalDigest, rhsAttachment.incrementalDigest)
|
||||
assertEquals(lhsAttachment.incrementalMacChunkSize, rhsAttachment.incrementalMacChunkSize)
|
||||
assertEquals(lhsAttachment.cdnNumber, rhsAttachment.cdnNumber)
|
||||
}
|
||||
|
||||
fun assertDoesNotHaveRemoteFields(attachmentId: AttachmentId) {
|
||||
val databaseAttachment = SignalDatabase.attachments.getAttachment(attachmentId)!!
|
||||
assertEquals(0, databaseAttachment.uploadTimestamp)
|
||||
assertNull(databaseAttachment.remoteLocation)
|
||||
assertNull(databaseAttachment.remoteDigest)
|
||||
assertNull(databaseAttachment.remoteKey)
|
||||
assertEquals(0, databaseAttachment.cdnNumber)
|
||||
}
|
||||
|
||||
fun assertSkipTransform(attachmentId: AttachmentId, state: Boolean) {
|
||||
val transformProperties = SignalDatabase.attachments.getTransformProperties(attachmentId)!!
|
||||
assertEquals("Incorrect skipTransform!", transformProperties.skipTransform, state)
|
||||
}
|
||||
|
||||
private fun ByteArray.asMediaStream(): MediaStream {
|
||||
return MediaStream(this.inputStream(), MediaUtil.IMAGE_JPEG, 2, 2)
|
||||
}
|
||||
|
||||
private fun createPointerAttachment(attachmentId: AttachmentId, uploadTimestamp: Long = System.currentTimeMillis()): PointerAttachment {
|
||||
val location = "somewhere-${Random.nextLong()}"
|
||||
val key = "somekey-${Random.nextLong()}"
|
||||
val digest = Random.nextBytes(32)
|
||||
val incrementalDigest = Random.nextBytes(16)
|
||||
|
||||
val databaseAttachment = SignalDatabase.attachments.getAttachment(attachmentId)!!
|
||||
|
||||
return PointerAttachment(
|
||||
"image/jpeg",
|
||||
AttachmentTable.TRANSFER_PROGRESS_DONE,
|
||||
databaseAttachment.size, // size
|
||||
null,
|
||||
3, // cdnNumber
|
||||
location,
|
||||
key,
|
||||
digest,
|
||||
incrementalDigest,
|
||||
5, // incrementalMacChunkSize
|
||||
null,
|
||||
databaseAttachment.voiceNote,
|
||||
databaseAttachment.borderless,
|
||||
databaseAttachment.videoGif,
|
||||
databaseAttachment.width,
|
||||
databaseAttachment.height,
|
||||
uploadTimestamp,
|
||||
databaseAttachment.caption,
|
||||
databaseAttachment.stickerLocator,
|
||||
databaseAttachment.blurHash
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun test(content: TestContext.() -> Unit) {
|
||||
SignalDatabase.attachments.deleteAllAttachments()
|
||||
val context = TestContext()
|
||||
context.content()
|
||||
}
|
||||
}
|
|
@ -52,7 +52,7 @@ class MessageContentProcessor__recipientStatusTest {
|
|||
processor.process(
|
||||
envelope = MessageContentFuzzer.envelope(envelopeTimestamp),
|
||||
content = MessageContentFuzzer.syncSentTextMessage(initialTextMessage, deliveredTo = listOf(harness.others[0])),
|
||||
metadata = MessageContentFuzzer.envelopeMetadata(harness.self.id, harness.self.id, groupId),
|
||||
metadata = MessageContentFuzzer.envelopeMetadata(harness.self.id, harness.self.id, groupId = groupId),
|
||||
serverDeliveredTimestamp = MessageContentFuzzer.fuzzServerDeliveredTimestamp(envelopeTimestamp)
|
||||
)
|
||||
|
||||
|
@ -64,7 +64,7 @@ class MessageContentProcessor__recipientStatusTest {
|
|||
processor.process(
|
||||
envelope = MessageContentFuzzer.envelope(envelopeTimestamp),
|
||||
content = MessageContentFuzzer.syncSentTextMessage(initialTextMessage, deliveredTo = listOf(harness.others[0], harness.others[1]), recipientUpdate = true),
|
||||
metadata = MessageContentFuzzer.envelopeMetadata(harness.self.id, harness.self.id, groupId),
|
||||
metadata = MessageContentFuzzer.envelopeMetadata(harness.self.id, harness.self.id, groupId = groupId),
|
||||
serverDeliveredTimestamp = MessageContentFuzzer.fuzzServerDeliveredTimestamp(envelopeTimestamp)
|
||||
)
|
||||
|
||||
|
|
|
@ -0,0 +1,225 @@
|
|||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.messages
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import io.mockk.every
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.slot
|
||||
import io.mockk.unmockkStatic
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.jobs.ThreadUpdateJob
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.testing.GroupTestingUtils
|
||||
import org.thoughtcrime.securesms.testing.MessageContentFuzzer
|
||||
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||
import org.thoughtcrime.securesms.testing.assertIs
|
||||
import java.util.UUID
|
||||
|
||||
@Suppress("ClassName")
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class SyncMessageProcessorTest_readSyncs {
|
||||
|
||||
@get:Rule
|
||||
val harness = SignalActivityRule(createGroup = true)
|
||||
|
||||
private lateinit var alice: RecipientId
|
||||
private lateinit var bob: RecipientId
|
||||
private lateinit var group: GroupTestingUtils.TestGroupInfo
|
||||
private lateinit var processor: MessageContentProcessor
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
alice = harness.others[0]
|
||||
bob = harness.others[1]
|
||||
group = harness.group!!
|
||||
|
||||
processor = MessageContentProcessor(harness.context)
|
||||
|
||||
val threadIdSlot = slot<Long>()
|
||||
mockkStatic(ThreadUpdateJob::class)
|
||||
every { ThreadUpdateJob.enqueue(capture(threadIdSlot)) } answers {
|
||||
SignalDatabase.threads.update(threadIdSlot.captured, false)
|
||||
}
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
unmockkStatic(ThreadUpdateJob::class)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleSynchronizeReadMessage() {
|
||||
val messageHelper = MessageHelper()
|
||||
|
||||
val message1Timestamp = messageHelper.incomingText().timestamp
|
||||
val message2Timestamp = messageHelper.incomingText().timestamp
|
||||
|
||||
val threadId = SignalDatabase.threads.getThreadIdFor(alice)!!
|
||||
var threadRecord = SignalDatabase.threads.getThreadRecord(threadId)!!
|
||||
threadRecord.unreadCount assertIs 2
|
||||
|
||||
messageHelper.syncReadMessage(alice to message1Timestamp, alice to message2Timestamp)
|
||||
|
||||
threadRecord = SignalDatabase.threads.getThreadRecord(threadId)!!
|
||||
threadRecord.unreadCount assertIs 0
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleSynchronizeReadMessageMissingTimestamp() {
|
||||
val messageHelper = MessageHelper()
|
||||
|
||||
messageHelper.incomingText().timestamp
|
||||
val message2Timestamp = messageHelper.incomingText().timestamp
|
||||
|
||||
val threadId = SignalDatabase.threads.getThreadIdFor(alice)!!
|
||||
var threadRecord = SignalDatabase.threads.getThreadRecord(threadId)!!
|
||||
threadRecord.unreadCount assertIs 2
|
||||
|
||||
messageHelper.syncReadMessage(alice to message2Timestamp)
|
||||
|
||||
threadRecord = SignalDatabase.threads.getThreadRecord(threadId)!!
|
||||
threadRecord.unreadCount assertIs 0
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleSynchronizeReadWithEdits() {
|
||||
val messageHelper = MessageHelper()
|
||||
|
||||
val message1Timestamp = messageHelper.incomingText().timestamp
|
||||
messageHelper.syncReadMessage(alice to message1Timestamp)
|
||||
|
||||
val editMessage1Timestamp1 = messageHelper.incomingEditText(message1Timestamp).timestamp
|
||||
val editMessage1Timestamp2 = messageHelper.incomingEditText(editMessage1Timestamp1).timestamp
|
||||
|
||||
val message2Timestamp = messageHelper.incomingMedia().timestamp
|
||||
|
||||
val threadId = SignalDatabase.threads.getThreadIdFor(alice)!!
|
||||
var threadRecord = SignalDatabase.threads.getThreadRecord(threadId)!!
|
||||
threadRecord.unreadCount assertIs 2
|
||||
|
||||
messageHelper.syncReadMessage(alice to message2Timestamp, alice to editMessage1Timestamp1, alice to editMessage1Timestamp2)
|
||||
|
||||
threadRecord = SignalDatabase.threads.getThreadRecord(threadId)!!
|
||||
threadRecord.unreadCount assertIs 0
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleSynchronizeReadWithEditsInGroup() {
|
||||
val messageHelper = MessageHelper()
|
||||
|
||||
val message1Timestamp = messageHelper.incomingText(sender = alice, destination = group.recipientId).timestamp
|
||||
|
||||
messageHelper.syncReadMessage(alice to message1Timestamp)
|
||||
|
||||
val editMessage1Timestamp1 = messageHelper.incomingEditText(targetTimestamp = message1Timestamp, sender = alice, destination = group.recipientId).timestamp
|
||||
val editMessage1Timestamp2 = messageHelper.incomingEditText(targetTimestamp = editMessage1Timestamp1, sender = alice, destination = group.recipientId).timestamp
|
||||
|
||||
val message2Timestamp = messageHelper.incomingMedia(sender = bob, destination = group.recipientId).timestamp
|
||||
|
||||
val threadId = SignalDatabase.threads.getThreadIdFor(group.recipientId)!!
|
||||
var threadRecord = SignalDatabase.threads.getThreadRecord(threadId)!!
|
||||
threadRecord.unreadCount assertIs 2
|
||||
|
||||
messageHelper.syncReadMessage(bob to message2Timestamp, alice to editMessage1Timestamp1, alice to editMessage1Timestamp2)
|
||||
|
||||
threadRecord = SignalDatabase.threads.getThreadRecord(threadId)!!
|
||||
threadRecord.unreadCount assertIs 0
|
||||
}
|
||||
|
||||
private inner class MessageHelper(var startTime: Long = System.currentTimeMillis()) {
|
||||
|
||||
fun incomingText(sender: RecipientId = alice, destination: RecipientId = harness.self.id): MessageData {
|
||||
startTime += 1000
|
||||
|
||||
val messageData = MessageData(timestamp = startTime)
|
||||
|
||||
processor.process(
|
||||
envelope = MessageContentFuzzer.envelope(messageData.timestamp, serverGuid = messageData.serverGuid),
|
||||
content = MessageContentFuzzer.fuzzTextMessage(
|
||||
sentTimestamp = messageData.timestamp,
|
||||
groupContextV2 = if (destination == group.recipientId) group.groupV2Context else null
|
||||
),
|
||||
metadata = MessageContentFuzzer.envelopeMetadata(
|
||||
source = sender,
|
||||
destination = harness.self.id,
|
||||
groupId = if (destination == group.recipientId) group.groupId else null
|
||||
),
|
||||
serverDeliveredTimestamp = messageData.timestamp + 10
|
||||
)
|
||||
|
||||
return messageData
|
||||
}
|
||||
|
||||
fun incomingMedia(sender: RecipientId = alice, destination: RecipientId = harness.self.id): MessageData {
|
||||
startTime += 1000
|
||||
|
||||
val messageData = MessageData(timestamp = startTime)
|
||||
|
||||
processor.process(
|
||||
envelope = MessageContentFuzzer.envelope(messageData.timestamp, serverGuid = messageData.serverGuid),
|
||||
content = MessageContentFuzzer.fuzzStickerMediaMessage(
|
||||
sentTimestamp = messageData.timestamp,
|
||||
groupContextV2 = if (destination == group.recipientId) group.groupV2Context else null
|
||||
),
|
||||
metadata = MessageContentFuzzer.envelopeMetadata(
|
||||
source = sender,
|
||||
destination = harness.self.id,
|
||||
groupId = if (destination == group.recipientId) group.groupId else null
|
||||
),
|
||||
serverDeliveredTimestamp = messageData.timestamp + 10
|
||||
)
|
||||
|
||||
return messageData
|
||||
}
|
||||
|
||||
fun incomingEditText(targetTimestamp: Long = System.currentTimeMillis(), sender: RecipientId = alice, destination: RecipientId = harness.self.id): MessageData {
|
||||
startTime += 1000
|
||||
|
||||
val messageData = MessageData(timestamp = startTime)
|
||||
|
||||
processor.process(
|
||||
envelope = MessageContentFuzzer.envelope(messageData.timestamp, serverGuid = messageData.serverGuid),
|
||||
content = MessageContentFuzzer.editTextMessage(
|
||||
targetTimestamp = targetTimestamp,
|
||||
editedDataMessage = MessageContentFuzzer.fuzzTextMessage(
|
||||
sentTimestamp = messageData.timestamp,
|
||||
groupContextV2 = if (destination == group.recipientId) group.groupV2Context else null
|
||||
).dataMessage!!
|
||||
),
|
||||
metadata = MessageContentFuzzer.envelopeMetadata(
|
||||
source = sender,
|
||||
destination = harness.self.id,
|
||||
groupId = if (destination == group.recipientId) group.groupId else null
|
||||
),
|
||||
serverDeliveredTimestamp = messageData.timestamp + 10
|
||||
)
|
||||
|
||||
return messageData
|
||||
}
|
||||
|
||||
fun syncReadMessage(vararg reads: Pair<RecipientId, Long>): MessageData {
|
||||
startTime += 1000
|
||||
val messageData = MessageData(timestamp = startTime)
|
||||
|
||||
processor.process(
|
||||
envelope = MessageContentFuzzer.envelope(messageData.timestamp, serverGuid = messageData.serverGuid),
|
||||
content = MessageContentFuzzer.syncReadsMessage(reads.toList()),
|
||||
metadata = MessageContentFuzzer.envelopeMetadata(harness.self.id, harness.self.id, sourceDeviceId = 2),
|
||||
serverDeliveredTimestamp = messageData.timestamp + 10
|
||||
)
|
||||
|
||||
return messageData
|
||||
}
|
||||
}
|
||||
|
||||
private data class MessageData(val serverGuid: UUID = UUID.randomUUID(), val timestamp: Long)
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
package org.thoughtcrime.securesms.testing
|
||||
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.signal.libsignal.zkgroup.groups.GroupMasterKey
|
||||
import org.signal.storageservice.protos.groups.Member
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup
|
||||
|
@ -9,6 +10,7 @@ import org.thoughtcrime.securesms.groups.GroupId
|
|||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||
import org.whispersystems.signalservice.internal.push.GroupContextV2
|
||||
import kotlin.random.Random
|
||||
|
||||
/**
|
||||
|
@ -46,5 +48,8 @@ object GroupTestingUtils {
|
|||
return member(aci = requireAci())
|
||||
}
|
||||
|
||||
data class TestGroupInfo(val groupId: GroupId.V2, val masterKey: GroupMasterKey, val recipientId: RecipientId)
|
||||
data class TestGroupInfo(val groupId: GroupId.V2, val masterKey: GroupMasterKey, val recipientId: RecipientId) {
|
||||
val groupV2Context: GroupContextV2
|
||||
get() = GroupContextV2(masterKey = masterKey.serialize().toByteString(), revision = 0)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import org.whispersystems.signalservice.internal.push.AttachmentPointer
|
|||
import org.whispersystems.signalservice.internal.push.BodyRange
|
||||
import org.whispersystems.signalservice.internal.push.Content
|
||||
import org.whispersystems.signalservice.internal.push.DataMessage
|
||||
import org.whispersystems.signalservice.internal.push.EditMessage
|
||||
import org.whispersystems.signalservice.internal.push.Envelope
|
||||
import org.whispersystems.signalservice.internal.push.GroupContextV2
|
||||
import org.whispersystems.signalservice.internal.push.SyncMessage
|
||||
|
@ -33,22 +34,22 @@ object MessageContentFuzzer {
|
|||
/**
|
||||
* Create an [Envelope].
|
||||
*/
|
||||
fun envelope(timestamp: Long): Envelope {
|
||||
fun envelope(timestamp: Long, serverGuid: UUID = UUID.randomUUID()): Envelope {
|
||||
return Envelope.Builder()
|
||||
.timestamp(timestamp)
|
||||
.serverTimestamp(timestamp + 5)
|
||||
.serverGuid(UUID.randomUUID().toString())
|
||||
.serverGuid(serverGuid.toString())
|
||||
.build()
|
||||
}
|
||||
|
||||
/**
|
||||
* Create metadata to match an [Envelope].
|
||||
*/
|
||||
fun envelopeMetadata(source: RecipientId, destination: RecipientId, groupId: GroupId.V2? = null): EnvelopeMetadata {
|
||||
fun envelopeMetadata(source: RecipientId, destination: RecipientId, sourceDeviceId: Int = 1, groupId: GroupId.V2? = null): EnvelopeMetadata {
|
||||
return EnvelopeMetadata(
|
||||
sourceServiceId = Recipient.resolved(source).requireServiceId(),
|
||||
sourceE164 = null,
|
||||
sourceDeviceId = 1,
|
||||
sourceDeviceId = sourceDeviceId,
|
||||
sealedSender = true,
|
||||
groupId = groupId?.decodedId,
|
||||
destinationServiceId = Recipient.resolved(destination).requireServiceId()
|
||||
|
@ -60,10 +61,11 @@ object MessageContentFuzzer {
|
|||
* - An expire timer value
|
||||
* - Bold style body ranges
|
||||
*/
|
||||
fun fuzzTextMessage(groupContextV2: GroupContextV2? = null): Content {
|
||||
fun fuzzTextMessage(sentTimestamp: Long? = null, groupContextV2: GroupContextV2? = null): Content {
|
||||
return Content.Builder()
|
||||
.dataMessage(
|
||||
DataMessage.Builder().buildWith {
|
||||
timestamp = sentTimestamp
|
||||
body = string()
|
||||
if (random.nextBoolean()) {
|
||||
expireTimer = random.nextInt(0..28.days.inWholeSeconds.toInt())
|
||||
|
@ -87,6 +89,20 @@ object MessageContentFuzzer {
|
|||
.build()
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an edit message.
|
||||
*/
|
||||
fun editTextMessage(targetTimestamp: Long, editedDataMessage: DataMessage): Content {
|
||||
return Content.Builder()
|
||||
.editMessage(
|
||||
EditMessage.Builder().buildWith {
|
||||
targetSentTimestamp = targetTimestamp
|
||||
dataMessage = editedDataMessage
|
||||
}
|
||||
)
|
||||
.build()
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a sync sent text message for the given [DataMessage].
|
||||
*/
|
||||
|
@ -116,6 +132,24 @@ object MessageContentFuzzer {
|
|||
).build()
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a sync reads message for the given [RecipientId] and message timestamp pairings.
|
||||
*/
|
||||
fun syncReadsMessage(timestamps: List<Pair<RecipientId, Long>>): Content {
|
||||
return Content
|
||||
.Builder()
|
||||
.syncMessage(
|
||||
SyncMessage.Builder().buildWith {
|
||||
read = timestamps.map { (senderId, timestamp) ->
|
||||
SyncMessage.Read.Builder().buildWith {
|
||||
this.senderAci = Recipient.resolved(senderId).requireAci().toString()
|
||||
this.timestamp = timestamp
|
||||
}
|
||||
}
|
||||
}
|
||||
).build()
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a random media message that may be:
|
||||
* - A text body
|
||||
|
@ -184,22 +218,21 @@ object MessageContentFuzzer {
|
|||
}
|
||||
|
||||
/**
|
||||
* Create a random media message that can never contain a text body. It may be:
|
||||
* - A sticker
|
||||
* Create a random media message that contains a sticker.
|
||||
*/
|
||||
fun fuzzMediaMessageNoText(previousMessages: List<TestMessage> = emptyList()): Content {
|
||||
fun fuzzStickerMediaMessage(sentTimestamp: Long? = null, groupContextV2: GroupContextV2? = null): Content {
|
||||
return Content.Builder()
|
||||
.dataMessage(
|
||||
DataMessage.Builder().buildWith {
|
||||
if (random.nextFloat() < 0.9) {
|
||||
sticker = DataMessage.Sticker.Builder().buildWith {
|
||||
packId = byteString(length = 24)
|
||||
packKey = byteString(length = 128)
|
||||
stickerId = random.nextInt()
|
||||
data_ = attachmentPointer()
|
||||
emoji = emojis.random(random)
|
||||
}
|
||||
timestamp = sentTimestamp
|
||||
sticker = DataMessage.Sticker.Builder().buildWith {
|
||||
packId = byteString(length = 24)
|
||||
packKey = byteString(length = 128)
|
||||
stickerId = random.nextInt()
|
||||
data_ = attachmentPointer()
|
||||
emoji = emojis.random(random)
|
||||
}
|
||||
groupV2 = groupContextV2
|
||||
}
|
||||
).build()
|
||||
}
|
||||
|
|
|
@ -2,6 +2,9 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
|
||||
<application
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:replace="android:usesCleartextTraffic"
|
||||
|
|
|
@ -681,6 +681,7 @@
|
|||
android:theme="@style/TextSecure.DarkNoActionBar"
|
||||
android:windowSoftInputMode="stateAlwaysHidden|adjustNothing"
|
||||
android:launchMode="singleTop"
|
||||
android:screenOrientation="portrait"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"
|
||||
android:exported="false"/>
|
||||
|
||||
|
@ -916,11 +917,20 @@
|
|||
android:windowSoftInputMode="stateVisible|adjustResize"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity android:name=".backup.v2.ui.MessageBackupsTestRestoreActivity"
|
||||
android:theme="@style/TextSecure.LightRegistrationTheme"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity android:name=".profiles.manage.EditProfileActivity"
|
||||
android:theme="@style/TextSecure.LightTheme"
|
||||
android:windowSoftInputMode="stateVisible|adjustResize"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity android:name=".nicknames.NicknameActivity"
|
||||
android:theme="@style/TextSecure.LightTheme"
|
||||
android:windowSoftInputMode="stateVisible|adjustResize"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity
|
||||
android:name=".payments.preferences.PaymentsActivity"
|
||||
android:theme="@style/TextSecure.LightRegistrationTheme"
|
||||
|
|
|
@ -68,6 +68,7 @@ import org.thoughtcrime.securesms.jobs.FcmRefreshJob;
|
|||
import org.thoughtcrime.securesms.jobs.FontDownloaderJob;
|
||||
import org.thoughtcrime.securesms.jobs.GroupRingCleanupJob;
|
||||
import org.thoughtcrime.securesms.jobs.GroupV2UpdateSelfProfileKeyJob;
|
||||
import org.thoughtcrime.securesms.jobs.LinkedDeviceInactiveCheckJob;
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
|
||||
import org.thoughtcrime.securesms.jobs.PnpInitializeDevicesJob;
|
||||
import org.thoughtcrime.securesms.jobs.PreKeysSyncJob;
|
||||
|
@ -241,6 +242,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
|||
.addPostRender(() -> ApplicationDependencies.getRecipientCache().warmUp())
|
||||
.addPostRender(AccountConsistencyWorkerJob::enqueueIfNecessary)
|
||||
.addPostRender(GroupRingCleanupJob::enqueue)
|
||||
.addPostRender(LinkedDeviceInactiveCheckJob::enqueueIfNecessary)
|
||||
.execute();
|
||||
|
||||
Log.d(TAG, "onCreateUnlock() took " + (System.currentTimeMillis() - startTime) + " ms");
|
||||
|
@ -345,7 +347,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
|||
public void checkBuildExpiration() {
|
||||
if (Util.getTimeUntilBuildExpiry() <= 0 && !SignalStore.misc().isClientDeprecated()) {
|
||||
Log.w(TAG, "Build expired!");
|
||||
SignalStore.misc().markClientDeprecated();
|
||||
SignalStore.misc().setClientDeprecated(true);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -28,6 +28,7 @@ import org.signal.libsignal.zkgroup.profiles.ProfileKey;
|
|||
import org.signal.qr.kitkat.ScanListener;
|
||||
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobs.LinkedDeviceInactiveCheckJob;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.signal.core.util.Base64;
|
||||
|
@ -233,6 +234,8 @@ public class DeviceActivity extends PassphraseRequiredActivity
|
|||
protected void onPostExecute(Integer result) {
|
||||
super.onPostExecute(result);
|
||||
|
||||
LinkedDeviceInactiveCheckJob.enqueue();
|
||||
|
||||
Context context = DeviceActivity.this;
|
||||
|
||||
switch (result) {
|
||||
|
|
|
@ -27,6 +27,7 @@ import org.signal.core.util.logging.Log;
|
|||
import org.thoughtcrime.securesms.database.loaders.DeviceListLoader;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.devicelist.Device;
|
||||
import org.thoughtcrime.securesms.jobs.LinkedDeviceInactiveCheckJob;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
|
||||
|
@ -167,6 +168,7 @@ public class DeviceListFragment extends ListFragment
|
|||
super.onPostExecute(result);
|
||||
if (result) {
|
||||
getLoaderManager().restartLoader(0, null, DeviceListFragment.this);
|
||||
LinkedDeviceInactiveCheckJob.enqueue();
|
||||
} else {
|
||||
Toast.makeText(getActivity(), R.string.DeviceListActivity_network_failed, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
|
|
@ -124,7 +124,7 @@ public class MainActivity extends PassphraseRequiredActivity implements VoiceNot
|
|||
.setMessage(R.string.OldDeviceTransferLockedDialog__your_signal_account_has_been_transferred_to_your_new_device)
|
||||
.setPositiveButton(R.string.OldDeviceTransferLockedDialog__done, (d, w) -> OldDeviceExitActivity.exit(this))
|
||||
.setNegativeButton(R.string.OldDeviceTransferLockedDialog__cancel_and_activate_this_device, (d, w) -> {
|
||||
SignalStore.misc().clearOldDeviceTransferLocked();
|
||||
SignalStore.misc().setOldDeviceTransferLocked(false);
|
||||
DeviceTransferBlockingInterceptor.getInstance().unblockNetwork();
|
||||
})
|
||||
.setCancelable(false)
|
||||
|
|
|
@ -139,6 +139,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
|||
public static final String EXTRA_ENABLE_VIDEO_IF_AVAILABLE = WebRtcCallActivity.class.getCanonicalName() + ".ENABLE_VIDEO_IF_AVAILABLE";
|
||||
public static final String EXTRA_STARTED_FROM_FULLSCREEN = WebRtcCallActivity.class.getCanonicalName() + ".STARTED_FROM_FULLSCREEN";
|
||||
public static final String EXTRA_STARTED_FROM_CALL_LINK = WebRtcCallActivity.class.getCanonicalName() + ".STARTED_FROM_CALL_LINK";
|
||||
public static final String EXTRA_LAUNCH_IN_PIP = WebRtcCallActivity.class.getCanonicalName() + ".STARTED_FROM_CALL_LINK";
|
||||
|
||||
private CallParticipantsListUpdatePopupWindow participantUpdateWindow;
|
||||
private CallStateUpdatePopupWindow callStateUpdatePopupWindow;
|
||||
|
@ -161,6 +162,8 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
|||
private LifecycleDisposable lifecycleDisposable;
|
||||
private long lastCallLinkDisconnectDialogShowTime;
|
||||
private ControlsAndInfoController controlsAndInfo;
|
||||
private boolean enterPipOnResume;
|
||||
private long lastProcessedIntentTimestamp;
|
||||
|
||||
private Disposable ephemeralStateDisposable = Disposable.empty();
|
||||
|
||||
|
@ -265,6 +268,11 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
|||
}
|
||||
}, TimeUnit.SECONDS.toMillis(1));
|
||||
}
|
||||
|
||||
if (enterPipOnResume) {
|
||||
enterPipOnResume = false;
|
||||
enterPipModeIfPossible();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -312,10 +320,16 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
|||
requestNewSizesThrottle.clear();
|
||||
}
|
||||
|
||||
ApplicationDependencies.getSignalCallManager().setEnableVideo(false);
|
||||
|
||||
if (!viewModel.isCallStarting()) {
|
||||
CallParticipantsState state = viewModel.getCallParticipantsStateSnapshot();
|
||||
if (state != null && state.getCallState().isPreJoinOrNetworkUnavailable()) {
|
||||
ApplicationDependencies.getSignalCallManager().cancelPreJoin();
|
||||
if (state != null) {
|
||||
if (state.getCallState().isPreJoinOrNetworkUnavailable()) {
|
||||
ApplicationDependencies.getSignalCallManager().cancelPreJoin();
|
||||
} else if (state.getCallState().getInOngoingCall() && isInPipMode()) {
|
||||
ApplicationDependencies.getSignalCallManager().relaunchPipOnForeground();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -379,6 +393,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
|||
Log.d(TAG, "Intent: Action: " + intent.getAction());
|
||||
Log.d(TAG, "Intent: EXTRA_STARTED_FROM_FULLSCREEN: " + intent.getBooleanExtra(EXTRA_STARTED_FROM_FULLSCREEN, false));
|
||||
Log.d(TAG, "Intent: EXTRA_ENABLE_VIDEO_IF_AVAILABLE: " + intent.getBooleanExtra(EXTRA_ENABLE_VIDEO_IF_AVAILABLE, false));
|
||||
Log.d(TAG, "Intent: EXTRA_LAUNCH_IN_PIP: " + intent.getBooleanExtra(EXTRA_LAUNCH_IN_PIP, false));
|
||||
}
|
||||
|
||||
private void processIntent(@NonNull Intent intent) {
|
||||
|
@ -391,6 +406,12 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
|||
} else if (END_CALL_ACTION.equals(action)) {
|
||||
handleEndCall();
|
||||
}
|
||||
|
||||
if (System.currentTimeMillis() - lastProcessedIntentTimestamp > TimeUnit.SECONDS.toMillis(1)) {
|
||||
enterPipOnResume = intent.getBooleanExtra(EXTRA_LAUNCH_IN_PIP, false);
|
||||
}
|
||||
|
||||
lastProcessedIntentTimestamp = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
private void initializePendingParticipantFragmentListener() {
|
||||
|
@ -861,8 +882,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
|||
}
|
||||
|
||||
private boolean isSystemPipEnabledAndAvailable() {
|
||||
return Build.VERSION.SDK_INT >= 26 &&
|
||||
getPackageManager().hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE);
|
||||
return Build.VERSION.SDK_INT >= 26 && getPackageManager().hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE);
|
||||
}
|
||||
|
||||
@RequiresApi(26)
|
||||
|
|
|
@ -3,13 +3,14 @@ package org.thoughtcrime.securesms.attachments
|
|||
import android.os.Parcelable
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import org.signal.core.util.DatabaseId
|
||||
|
||||
@Parcelize
|
||||
data class AttachmentId(
|
||||
@JsonProperty("rowId")
|
||||
@JvmField
|
||||
val id: Long
|
||||
) : Parcelable {
|
||||
) : Parcelable, DatabaseId {
|
||||
|
||||
val isValid: Boolean
|
||||
get() = id >= 0
|
||||
|
@ -17,4 +18,8 @@ data class AttachmentId(
|
|||
override fun toString(): String {
|
||||
return "AttachmentId::$id"
|
||||
}
|
||||
|
||||
override fun serialize(): String {
|
||||
return id.toString()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.attachments
|
|||
|
||||
import android.net.Uri
|
||||
import android.os.Parcel
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import org.signal.core.util.Base64.encodeWithPadding
|
||||
import org.thoughtcrime.securesms.blurhash.BlurHash
|
||||
import org.thoughtcrime.securesms.database.AttachmentTable
|
||||
|
@ -14,7 +15,8 @@ import org.whispersystems.signalservice.internal.push.DataMessage
|
|||
import java.util.Optional
|
||||
|
||||
class PointerAttachment : Attachment {
|
||||
private constructor(
|
||||
@VisibleForTesting
|
||||
constructor(
|
||||
contentType: String,
|
||||
transferState: Int,
|
||||
size: Long,
|
||||
|
|
|
@ -198,7 +198,7 @@ public class FullBackupImporter extends FullBackupBase {
|
|||
private static void processAttachment(@NonNull Context context, @NonNull AttachmentSecret attachmentSecret, @NonNull SQLiteDatabase db, @NonNull Attachment attachment, BackupRecordInputStream inputStream)
|
||||
throws IOException
|
||||
{
|
||||
File dataFile = AttachmentTable.newFile(context);
|
||||
File dataFile = AttachmentTable.newDataFile(context);
|
||||
Pair<byte[], OutputStream> output = ModernEncryptingPartOutputStream.createFor(attachmentSecret, dataFile, false);
|
||||
boolean isLegacyTable = SqlUtil.tableExists(db, "part");
|
||||
|
||||
|
|
|
@ -70,13 +70,13 @@ object BackupRepository {
|
|||
)
|
||||
}
|
||||
|
||||
val exportState = ExportState()
|
||||
val exportState = ExportState(System.currentTimeMillis())
|
||||
|
||||
writer.use {
|
||||
writer.write(
|
||||
BackupInfo(
|
||||
version = VERSION,
|
||||
backupTimeMs = System.currentTimeMillis()
|
||||
backupTimeMs = exportState.backupTime
|
||||
)
|
||||
)
|
||||
// Note: Without a transaction, we may export inconsistent state. But because we have a transaction,
|
||||
|
@ -118,7 +118,7 @@ object BackupRepository {
|
|||
val masterKey = SignalStore.svr().getOrCreateMasterKey()
|
||||
val key = MessageBackupKey(masterKey.serialize(), Aci.parseFromBinary(selfData.aci.toByteArray()))
|
||||
|
||||
return MessageBackup.validate(key, inputStreamFactory, length)
|
||||
return MessageBackup.validate(key, MessageBackup.Purpose.REMOTE_BACKUP, inputStreamFactory, length)
|
||||
}
|
||||
|
||||
fun import(length: Long, inputStreamFactory: () -> InputStream, selfData: SelfData, plaintext: Boolean = false) {
|
||||
|
@ -396,7 +396,7 @@ object BackupRepository {
|
|||
}
|
||||
}
|
||||
|
||||
class ExportState {
|
||||
class ExportState(val backupTime: Long) {
|
||||
val recipientIds = HashSet<Long>()
|
||||
val threadIds = HashSet<Long>()
|
||||
}
|
||||
|
|
|
@ -17,12 +17,15 @@ import org.signal.core.util.requireBoolean
|
|||
import org.signal.core.util.requireInt
|
||||
import org.signal.core.util.requireLong
|
||||
import org.signal.core.util.requireString
|
||||
import org.thoughtcrime.securesms.attachments.DatabaseAttachment
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.CallChatUpdate
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.ChatItem
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.ChatUpdateMessage
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.ExpirationTimerChatUpdate
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.FilePointer
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.GroupCallChatUpdate
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.IndividualCallChatUpdate
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.MessageAttachment
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.ProfileChangeChatUpdate
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.Quote
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.Reaction
|
||||
|
@ -106,6 +109,7 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
|
|||
|
||||
val reactionsById: Map<Long, List<ReactionRecord>> = SignalDatabase.reactions.getReactionsForMessages(records.keys)
|
||||
val mentionsById: Map<Long, List<Mention>> = SignalDatabase.mentions.getMentionsForMessages(records.keys)
|
||||
val attachmentsById: Map<Long, List<DatabaseAttachment>> = SignalDatabase.attachments.getAttachmentsForMessages(records.keys)
|
||||
val groupReceiptsById: Map<Long, List<GroupReceiptTable.GroupReceiptInfo>> = SignalDatabase.groupReceipts.getGroupReceiptInfoForMessages(records.keys)
|
||||
|
||||
for ((id, record) in records) {
|
||||
|
@ -117,14 +121,23 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
|
|||
MessageTypes.isIdentityUpdate(record.type) -> builder.updateMessage = ChatUpdateMessage(simpleUpdate = SimpleChatUpdate(type = SimpleChatUpdate.Type.IDENTITY_UPDATE))
|
||||
MessageTypes.isIdentityVerified(record.type) -> builder.updateMessage = ChatUpdateMessage(simpleUpdate = SimpleChatUpdate(type = SimpleChatUpdate.Type.IDENTITY_VERIFIED))
|
||||
MessageTypes.isIdentityDefault(record.type) -> builder.updateMessage = ChatUpdateMessage(simpleUpdate = SimpleChatUpdate(type = SimpleChatUpdate.Type.IDENTITY_DEFAULT))
|
||||
MessageTypes.isChangeNumber(record.type) -> builder.updateMessage = ChatUpdateMessage(simpleUpdate = SimpleChatUpdate(type = SimpleChatUpdate.Type.CHANGE_NUMBER))
|
||||
MessageTypes.isBoostRequest(record.type) -> builder.updateMessage = ChatUpdateMessage(simpleUpdate = SimpleChatUpdate(type = SimpleChatUpdate.Type.BOOST_REQUEST))
|
||||
MessageTypes.isChangeNumber(record.type) -> {
|
||||
builder.updateMessage = ChatUpdateMessage(simpleUpdate = SimpleChatUpdate(type = SimpleChatUpdate.Type.CHANGE_NUMBER))
|
||||
builder.sms = false
|
||||
}
|
||||
MessageTypes.isBoostRequest(record.type) -> {
|
||||
builder.updateMessage = ChatUpdateMessage(simpleUpdate = SimpleChatUpdate(type = SimpleChatUpdate.Type.BOOST_REQUEST))
|
||||
builder.sms = false
|
||||
}
|
||||
MessageTypes.isEndSessionType(record.type) -> builder.updateMessage = ChatUpdateMessage(simpleUpdate = SimpleChatUpdate(type = SimpleChatUpdate.Type.END_SESSION))
|
||||
MessageTypes.isChatSessionRefresh(record.type) -> builder.updateMessage = ChatUpdateMessage(simpleUpdate = SimpleChatUpdate(type = SimpleChatUpdate.Type.CHAT_SESSION_REFRESH))
|
||||
MessageTypes.isBadDecryptType(record.type) -> builder.updateMessage = ChatUpdateMessage(simpleUpdate = SimpleChatUpdate(type = SimpleChatUpdate.Type.BAD_DECRYPT))
|
||||
MessageTypes.isPaymentsActivated(record.type) -> builder.updateMessage = ChatUpdateMessage(simpleUpdate = SimpleChatUpdate(type = SimpleChatUpdate.Type.PAYMENTS_ACTIVATED))
|
||||
MessageTypes.isPaymentsRequestToActivate(record.type) -> builder.updateMessage = ChatUpdateMessage(simpleUpdate = SimpleChatUpdate(type = SimpleChatUpdate.Type.PAYMENT_ACTIVATION_REQUEST))
|
||||
MessageTypes.isExpirationTimerUpdate(record.type) -> builder.updateMessage = ChatUpdateMessage(expirationTimerChange = ExpirationTimerChatUpdate((record.expiresIn / 1000).toInt()))
|
||||
MessageTypes.isExpirationTimerUpdate(record.type) -> {
|
||||
builder.updateMessage = ChatUpdateMessage(expirationTimerChange = ExpirationTimerChatUpdate(record.expiresIn.toInt()))
|
||||
builder.expiresInMs = null
|
||||
}
|
||||
MessageTypes.isProfileChange(record.type) -> {
|
||||
builder.updateMessage = ChatUpdateMessage(
|
||||
profileChange = try {
|
||||
|
@ -140,6 +153,7 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
|
|||
ProfileChangeChatUpdate()
|
||||
}
|
||||
)
|
||||
builder.sms = false
|
||||
}
|
||||
MessageTypes.isSessionSwitchoverType(record.type) -> {
|
||||
builder.updateMessage = ChatUpdateMessage(
|
||||
|
@ -188,10 +202,10 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
|
|||
} else {
|
||||
when {
|
||||
MessageTypes.isMissedAudioCall(record.type) -> {
|
||||
builder.updateMessage = ChatUpdateMessage(callingMessage = CallChatUpdate(callMessage = IndividualCallChatUpdate(type = IndividualCallChatUpdate.Type.MISSED_AUDIO_CALL)))
|
||||
builder.updateMessage = ChatUpdateMessage(callingMessage = CallChatUpdate(callMessage = IndividualCallChatUpdate(type = IndividualCallChatUpdate.Type.MISSED_INCOMING_AUDIO_CALL)))
|
||||
}
|
||||
MessageTypes.isMissedVideoCall(record.type) -> {
|
||||
builder.updateMessage = ChatUpdateMessage(callingMessage = CallChatUpdate(callMessage = IndividualCallChatUpdate(type = IndividualCallChatUpdate.Type.MISSED_VIDEO_CALL)))
|
||||
builder.updateMessage = ChatUpdateMessage(callingMessage = CallChatUpdate(callMessage = IndividualCallChatUpdate(type = IndividualCallChatUpdate.Type.MISSED_INCOMING_VIDEO_CALL)))
|
||||
}
|
||||
MessageTypes.isIncomingAudioCall(record.type) -> {
|
||||
builder.updateMessage = ChatUpdateMessage(callingMessage = CallChatUpdate(callMessage = IndividualCallChatUpdate(type = IndividualCallChatUpdate.Type.INCOMING_AUDIO_CALL)))
|
||||
|
@ -230,11 +244,11 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
|
|||
}
|
||||
}
|
||||
}
|
||||
record.body == null -> {
|
||||
Log.w(TAG, "Record missing a body, skipping")
|
||||
record.body == null && !attachmentsById.containsKey(record.id) -> {
|
||||
Log.w(TAG, "Record missing a body and doesnt have attachments, skipping")
|
||||
continue
|
||||
}
|
||||
else -> builder.standardMessage = record.toTextMessage(reactionsById[id], mentions = mentionsById[id])
|
||||
else -> builder.standardMessage = record.toStandardMessage(reactionsById[id], mentions = mentionsById[id], attachments = attachmentsById[record.id])
|
||||
}
|
||||
|
||||
buffer += builder.build()
|
||||
|
@ -268,7 +282,6 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
|
|||
chatId = record.threadId
|
||||
authorId = record.fromRecipientId
|
||||
dateSent = record.dateSent
|
||||
sealedSender = record.sealedSender
|
||||
expireStartDate = if (record.expireStarted > 0) record.expireStarted else null
|
||||
expiresInMs = if (record.expiresIn > 0) record.expiresIn else null
|
||||
revisions = emptyList()
|
||||
|
@ -282,19 +295,28 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
|
|||
incoming = ChatItem.IncomingMessageDetails(
|
||||
dateServerSent = record.dateServer,
|
||||
dateReceived = record.dateReceived,
|
||||
read = record.read
|
||||
read = record.read,
|
||||
sealedSender = record.sealedSender
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun BackupMessageRecord.toTextMessage(reactionRecords: List<ReactionRecord>?, mentions: List<Mention>?): StandardMessage {
|
||||
return StandardMessage(
|
||||
quote = this.toQuote(),
|
||||
text = Text(
|
||||
body = this.body!!,
|
||||
private fun BackupMessageRecord.toStandardMessage(reactionRecords: List<ReactionRecord>?, mentions: List<Mention>?, attachments: List<DatabaseAttachment>?): StandardMessage {
|
||||
val text = if (body == null) {
|
||||
null
|
||||
} else {
|
||||
Text(
|
||||
body = this.body,
|
||||
bodyRanges = (this.bodyRanges?.toBackupBodyRanges() ?: emptyList()) + (mentions?.toBackupBodyRanges() ?: emptyList())
|
||||
),
|
||||
)
|
||||
}
|
||||
val quotedAttachments = attachments?.filter { it.quote } ?: emptyList()
|
||||
val messageAttachments = attachments?.filter { !it.quote } ?: emptyList()
|
||||
return StandardMessage(
|
||||
quote = this.toQuote(quotedAttachments),
|
||||
text = text,
|
||||
attachments = messageAttachments.toBackupAttachments(),
|
||||
// TODO Link previews!
|
||||
linkPreview = emptyList(),
|
||||
longText = null,
|
||||
|
@ -302,14 +324,14 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
|
|||
)
|
||||
}
|
||||
|
||||
private fun BackupMessageRecord.toQuote(): Quote? {
|
||||
private fun BackupMessageRecord.toQuote(attachments: List<DatabaseAttachment>? = null): Quote? {
|
||||
return if (this.quoteTargetSentTimestamp != MessageTable.QUOTE_NOT_PRESENT_ID && this.quoteAuthor > 0) {
|
||||
// TODO Attachments!
|
||||
val type = QuoteModel.Type.fromCode(this.quoteType)
|
||||
Quote(
|
||||
targetSentTimestamp = this.quoteTargetSentTimestamp.takeIf { !this.quoteMissing && it != MessageTable.QUOTE_TARGET_MISSING_ID },
|
||||
authorId = this.quoteAuthor,
|
||||
text = this.quoteBody,
|
||||
attachments = attachments?.toBackupQuoteAttachments() ?: emptyList(),
|
||||
bodyRanges = this.quoteBodyRanges?.toBackupBodyRanges() ?: emptyList(),
|
||||
type = when (type) {
|
||||
QuoteModel.Type.NORMAL -> Quote.Type.NORMAL
|
||||
|
@ -321,6 +343,44 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
|
|||
}
|
||||
}
|
||||
|
||||
private fun List<DatabaseAttachment>.toBackupQuoteAttachments(): List<Quote.QuotedAttachment> {
|
||||
return this.map { attachment ->
|
||||
Quote.QuotedAttachment(
|
||||
contentType = attachment.contentType,
|
||||
fileName = attachment.fileName,
|
||||
thumbnail = attachment.toBackupAttachment()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun DatabaseAttachment.toBackupAttachment(): MessageAttachment {
|
||||
return MessageAttachment(
|
||||
pointer = FilePointer(
|
||||
attachmentLocator = FilePointer.AttachmentLocator(
|
||||
cdnKey = this.remoteLocation ?: "",
|
||||
cdnNumber = this.cdnNumber,
|
||||
uploadTimestamp = this.uploadTimestamp
|
||||
),
|
||||
key = if (remoteKey != null) decode(remoteKey).toByteString() else null,
|
||||
contentType = this.contentType,
|
||||
size = this.size.toInt(),
|
||||
incrementalMac = this.incrementalDigest?.toByteString(),
|
||||
incrementalMacChunkSize = this.incrementalMacChunkSize,
|
||||
fileName = this.fileName,
|
||||
width = this.width,
|
||||
height = this.height,
|
||||
caption = this.caption,
|
||||
blurHash = this.blurHash?.hash
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun List<DatabaseAttachment>.toBackupAttachments(): List<MessageAttachment> {
|
||||
return this.map { attachment ->
|
||||
attachment.toBackupAttachment()
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<Mention>.toBackupBodyRanges(): List<BackupBodyRange> {
|
||||
return this.map {
|
||||
BackupBodyRange(
|
||||
|
|
|
@ -10,13 +10,17 @@ import androidx.core.content.contentValuesOf
|
|||
import org.signal.core.util.Base64
|
||||
import org.signal.core.util.SqlUtil
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.orNull
|
||||
import org.signal.core.util.requireLong
|
||||
import org.signal.core.util.toInt
|
||||
import org.thoughtcrime.securesms.attachments.Attachment
|
||||
import org.thoughtcrime.securesms.attachments.PointerAttachment
|
||||
import org.thoughtcrime.securesms.backup.v2.BackupState
|
||||
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.IndividualCallChatUpdate
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.MessageAttachment
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.Quote
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.Reaction
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.SendStatus
|
||||
|
@ -44,8 +48,12 @@ import org.thoughtcrime.securesms.mms.QuoteModel
|
|||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.util.JsonUtils
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil
|
||||
import java.util.Optional
|
||||
|
||||
/**
|
||||
* An object that will ingest all fo the [ChatItem]s you want to write, buffer them until hitting a specified batch size, and then batch insert them
|
||||
|
@ -228,6 +236,17 @@ class ChatItemImportInserter(
|
|||
}
|
||||
}
|
||||
}
|
||||
val attachments = this.standardMessage.attachments.mapNotNull { attachment ->
|
||||
attachment.toLocalAttachment()
|
||||
}
|
||||
val quoteAttachments = this.standardMessage.quote?.attachments?.mapNotNull {
|
||||
it.toLocalAttachment()
|
||||
} ?: emptyList()
|
||||
if (attachments.isNotEmpty()) {
|
||||
followUp = { messageRowId ->
|
||||
SignalDatabase.attachments.insertAttachmentsForMessage(messageRowId, attachments, quoteAttachments)
|
||||
}
|
||||
}
|
||||
}
|
||||
return MessageInsert(contentValues, followUp)
|
||||
}
|
||||
|
@ -244,7 +263,7 @@ class ChatItemImportInserter(
|
|||
contentValues.put(MessageTable.TO_RECIPIENT_ID, (if (this.outgoing != null) chatRecipientId else selfId).serialize())
|
||||
contentValues.put(MessageTable.THREAD_ID, threadId)
|
||||
contentValues.put(MessageTable.DATE_RECEIVED, this.incoming?.dateReceived ?: this.dateSent)
|
||||
contentValues.put(MessageTable.RECEIPT_TIMESTAMP, this.outgoing?.sendStatus?.maxOf { it.lastStatusUpdateTimestamp } ?: 0)
|
||||
contentValues.put(MessageTable.RECEIPT_TIMESTAMP, this.outgoing?.sendStatus?.maxOfOrNull { it.lastStatusUpdateTimestamp } ?: 0)
|
||||
contentValues.putNull(MessageTable.LATEST_REVISION_ID)
|
||||
contentValues.putNull(MessageTable.ORIGINAL_MESSAGE_ID)
|
||||
contentValues.put(MessageTable.REVISION_NUMBER, 0)
|
||||
|
@ -268,7 +287,7 @@ class ChatItemImportInserter(
|
|||
contentValues.put(MessageTable.VIEWED_COLUMN, 0)
|
||||
contentValues.put(MessageTable.HAS_READ_RECEIPT, 0)
|
||||
contentValues.put(MessageTable.HAS_DELIVERY_RECEIPT, 0)
|
||||
contentValues.put(MessageTable.UNIDENTIFIED, this.sealedSender?.toInt())
|
||||
contentValues.put(MessageTable.UNIDENTIFIED, this.incoming?.sealedSender?.toInt() ?: 0)
|
||||
contentValues.put(MessageTable.READ, this.incoming?.read?.toInt() ?: 0)
|
||||
contentValues.put(MessageTable.NOTIFIED, 1)
|
||||
}
|
||||
|
@ -382,23 +401,24 @@ class ChatItemImportInserter(
|
|||
var typeFlags: Long = 0
|
||||
when {
|
||||
updateMessage.simpleUpdate != null -> {
|
||||
val typeWithoutBase = (getAsLong(MessageTable.TYPE) and MessageTypes.BASE_TYPE_MASK.inv())
|
||||
typeFlags = when (updateMessage.simpleUpdate.type) {
|
||||
SimpleChatUpdate.Type.UNKNOWN -> 0
|
||||
SimpleChatUpdate.Type.JOINED_SIGNAL -> MessageTypes.JOINED_TYPE
|
||||
SimpleChatUpdate.Type.IDENTITY_UPDATE -> MessageTypes.KEY_EXCHANGE_IDENTITY_UPDATE_BIT
|
||||
SimpleChatUpdate.Type.IDENTITY_VERIFIED -> MessageTypes.KEY_EXCHANGE_IDENTITY_VERIFIED_BIT
|
||||
SimpleChatUpdate.Type.IDENTITY_DEFAULT -> MessageTypes.KEY_EXCHANGE_IDENTITY_DEFAULT_BIT
|
||||
SimpleChatUpdate.Type.UNKNOWN -> typeWithoutBase
|
||||
SimpleChatUpdate.Type.JOINED_SIGNAL -> MessageTypes.JOINED_TYPE or typeWithoutBase
|
||||
SimpleChatUpdate.Type.IDENTITY_UPDATE -> MessageTypes.KEY_EXCHANGE_IDENTITY_UPDATE_BIT or typeWithoutBase
|
||||
SimpleChatUpdate.Type.IDENTITY_VERIFIED -> MessageTypes.KEY_EXCHANGE_IDENTITY_VERIFIED_BIT or typeWithoutBase
|
||||
SimpleChatUpdate.Type.IDENTITY_DEFAULT -> MessageTypes.KEY_EXCHANGE_IDENTITY_DEFAULT_BIT or typeWithoutBase
|
||||
SimpleChatUpdate.Type.CHANGE_NUMBER -> MessageTypes.CHANGE_NUMBER_TYPE
|
||||
SimpleChatUpdate.Type.BOOST_REQUEST -> MessageTypes.BOOST_REQUEST_TYPE
|
||||
SimpleChatUpdate.Type.END_SESSION -> MessageTypes.END_SESSION_BIT
|
||||
SimpleChatUpdate.Type.CHAT_SESSION_REFRESH -> MessageTypes.ENCRYPTION_REMOTE_FAILED_BIT
|
||||
SimpleChatUpdate.Type.BAD_DECRYPT -> MessageTypes.BAD_DECRYPT_TYPE
|
||||
SimpleChatUpdate.Type.PAYMENTS_ACTIVATED -> MessageTypes.SPECIAL_TYPE_PAYMENTS_ACTIVATED
|
||||
SimpleChatUpdate.Type.PAYMENT_ACTIVATION_REQUEST -> MessageTypes.SPECIAL_TYPE_PAYMENTS_ACTIVATE_REQUEST
|
||||
SimpleChatUpdate.Type.END_SESSION -> MessageTypes.END_SESSION_BIT or typeWithoutBase
|
||||
SimpleChatUpdate.Type.CHAT_SESSION_REFRESH -> MessageTypes.ENCRYPTION_REMOTE_FAILED_BIT or typeWithoutBase
|
||||
SimpleChatUpdate.Type.BAD_DECRYPT -> MessageTypes.BAD_DECRYPT_TYPE or typeWithoutBase
|
||||
SimpleChatUpdate.Type.PAYMENTS_ACTIVATED -> MessageTypes.SPECIAL_TYPE_PAYMENTS_ACTIVATED or typeWithoutBase
|
||||
SimpleChatUpdate.Type.PAYMENT_ACTIVATION_REQUEST -> MessageTypes.SPECIAL_TYPE_PAYMENTS_ACTIVATE_REQUEST or typeWithoutBase
|
||||
}
|
||||
}
|
||||
updateMessage.expirationTimerChange != null -> {
|
||||
typeFlags = MessageTypes.EXPIRATION_TIMER_UPDATE_BIT
|
||||
typeFlags = getAsLong(MessageTable.TYPE) or MessageTypes.EXPIRATION_TIMER_UPDATE_BIT
|
||||
put(MessageTable.EXPIRES_IN, updateMessage.expirationTimerChange.expiresInMs.toLong())
|
||||
}
|
||||
updateMessage.profileChange != null -> {
|
||||
|
@ -408,12 +428,12 @@ class ChatItemImportInserter(
|
|||
put(MessageTable.BODY, Base64.encodeWithPadding(profileChangeDetails))
|
||||
}
|
||||
updateMessage.sessionSwitchover != null -> {
|
||||
typeFlags = MessageTypes.SESSION_SWITCHOVER_TYPE
|
||||
typeFlags = MessageTypes.SESSION_SWITCHOVER_TYPE or (getAsLong(MessageTable.TYPE) and MessageTypes.BASE_TYPE_MASK.inv())
|
||||
val sessionSwitchoverDetails = SessionSwitchoverEvent(e164 = updateMessage.sessionSwitchover.e164.toString()).encode()
|
||||
put(MessageTable.BODY, Base64.encodeWithPadding(sessionSwitchoverDetails))
|
||||
}
|
||||
updateMessage.threadMerge != null -> {
|
||||
typeFlags = MessageTypes.THREAD_MERGE_TYPE
|
||||
typeFlags = MessageTypes.THREAD_MERGE_TYPE or (getAsLong(MessageTable.TYPE) and MessageTypes.BASE_TYPE_MASK.inv())
|
||||
val threadMergeDetails = ThreadMergeEvent(previousE164 = updateMessage.threadMerge.previousE164.toString()).encode()
|
||||
put(MessageTable.BODY, Base64.encodeWithPadding(threadMergeDetails))
|
||||
}
|
||||
|
@ -428,8 +448,10 @@ class ChatItemImportInserter(
|
|||
IndividualCallChatUpdate.Type.INCOMING_VIDEO_CALL -> MessageTypes.INCOMING_VIDEO_CALL_TYPE
|
||||
IndividualCallChatUpdate.Type.OUTGOING_AUDIO_CALL -> MessageTypes.OUTGOING_AUDIO_CALL_TYPE
|
||||
IndividualCallChatUpdate.Type.OUTGOING_VIDEO_CALL -> MessageTypes.OUTGOING_VIDEO_CALL_TYPE
|
||||
IndividualCallChatUpdate.Type.MISSED_AUDIO_CALL -> MessageTypes.MISSED_AUDIO_CALL_TYPE
|
||||
IndividualCallChatUpdate.Type.MISSED_VIDEO_CALL -> MessageTypes.MISSED_VIDEO_CALL_TYPE
|
||||
IndividualCallChatUpdate.Type.MISSED_INCOMING_AUDIO_CALL -> MessageTypes.MISSED_AUDIO_CALL_TYPE
|
||||
IndividualCallChatUpdate.Type.MISSED_INCOMING_VIDEO_CALL -> MessageTypes.MISSED_VIDEO_CALL_TYPE
|
||||
IndividualCallChatUpdate.Type.UNANSWERED_OUTGOING_AUDIO_CALL -> MessageTypes.OUTGOING_AUDIO_CALL_TYPE
|
||||
IndividualCallChatUpdate.Type.UNANSWERED_OUTGOING_VIDEO_CALL -> MessageTypes.OUTGOING_VIDEO_CALL_TYPE
|
||||
IndividualCallChatUpdate.Type.UNKNOWN -> typeFlags
|
||||
}
|
||||
}
|
||||
|
@ -446,10 +468,10 @@ class ChatItemImportInserter(
|
|||
GV2UpdateDescription(groupChangeUpdate = updateMessage.groupChange)
|
||||
).encode()
|
||||
)
|
||||
typeFlags = MessageTypes.GROUP_V2_BIT or MessageTypes.GROUP_UPDATE_BIT
|
||||
typeFlags = getAsLong(MessageTable.TYPE) or MessageTypes.GROUP_V2_BIT or MessageTypes.GROUP_UPDATE_BIT
|
||||
}
|
||||
}
|
||||
this.put(MessageTable.TYPE, getAsLong(MessageTable.TYPE) or typeFlags)
|
||||
this.put(MessageTable.TYPE, typeFlags)
|
||||
}
|
||||
|
||||
private fun ContentValues.addQuote(quote: Quote) {
|
||||
|
@ -508,7 +530,7 @@ class ChatItemImportInserter(
|
|||
}
|
||||
|
||||
return BodyRangeList(
|
||||
ranges = this.map { bodyRange ->
|
||||
ranges = this.filter { it.mentionAci == null }.map { bodyRange ->
|
||||
BodyRangeList.BodyRange(
|
||||
mentionUuid = bodyRange.mentionAci?.let { UuidUtil.fromByteString(it) }?.toString(),
|
||||
style = bodyRange.style?.let {
|
||||
|
@ -541,6 +563,39 @@ class ChatItemImportInserter(
|
|||
}
|
||||
}
|
||||
|
||||
private fun MessageAttachment.toLocalAttachment(contentType: String? = pointer?.contentType, fileName: String? = pointer?.fileName): Attachment? {
|
||||
if (pointer == null) return null
|
||||
if (pointer.attachmentLocator != null) {
|
||||
val signalAttachmentPointer = SignalServiceAttachmentPointer(
|
||||
pointer.attachmentLocator.cdnNumber,
|
||||
SignalServiceAttachmentRemoteId.from(pointer.attachmentLocator.cdnKey),
|
||||
contentType,
|
||||
pointer.key?.toByteArray(),
|
||||
Optional.ofNullable(pointer.size),
|
||||
Optional.empty(),
|
||||
pointer.width ?: 0,
|
||||
pointer.height ?: 0,
|
||||
Optional.empty(),
|
||||
Optional.ofNullable(pointer.incrementalMac?.toByteArray()),
|
||||
pointer.incrementalMacChunkSize ?: 0,
|
||||
Optional.ofNullable(fileName),
|
||||
flag == MessageAttachment.Flag.VOICE_MESSAGE,
|
||||
flag == MessageAttachment.Flag.BORDERLESS,
|
||||
flag == MessageAttachment.Flag.GIF,
|
||||
Optional.ofNullable(pointer.caption),
|
||||
Optional.ofNullable(pointer.blurHash),
|
||||
pointer.attachmentLocator.uploadTimestamp
|
||||
)
|
||||
return PointerAttachment.forPointer(Optional.of(signalAttachmentPointer)).orNull()
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun Quote.QuotedAttachment.toLocalAttachment(): Attachment? {
|
||||
return thumbnail?.toLocalAttachment(this.contentType, this.fileName)
|
||||
?: if (this.contentType == null) null else PointerAttachment.forPointer(SignalServiceDataMessage.Quote.QuotedAttachment(contentType = this.contentType!!, fileName = this.fileName, thumbnail = null)).orNull()
|
||||
}
|
||||
|
||||
private class MessageInsert(val contentValues: ContentValues, val followUp: ((Long) -> Unit)?)
|
||||
|
||||
private class Buffer(
|
||||
|
|
|
@ -11,11 +11,12 @@ import org.signal.core.util.select
|
|||
import org.thoughtcrime.securesms.backup.v2.BackupState
|
||||
import org.thoughtcrime.securesms.database.MessageTable
|
||||
import org.thoughtcrime.securesms.database.MessageTypes
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
private val TAG = Log.tag(MessageTable::class.java)
|
||||
private const val BASE_TYPE = "base_type"
|
||||
|
||||
fun MessageTable.getMessagesForBackup(): ChatItemExportIterator {
|
||||
fun MessageTable.getMessagesForBackup(backupTime: Long): ChatItemExportIterator {
|
||||
val cursor = readableDatabase
|
||||
.select(
|
||||
MessageTable.ID,
|
||||
|
@ -53,13 +54,11 @@ fun MessageTable.getMessagesForBackup(): ChatItemExportIterator {
|
|||
.from(MessageTable.TABLE_NAME)
|
||||
.where(
|
||||
"""
|
||||
$BASE_TYPE IN (
|
||||
${MessageTypes.BASE_INBOX_TYPE},
|
||||
${MessageTypes.BASE_OUTBOX_TYPE},
|
||||
${MessageTypes.BASE_SENT_TYPE},
|
||||
${MessageTypes.BASE_SENDING_TYPE},
|
||||
${MessageTypes.BASE_SENT_FAILED_TYPE}
|
||||
) OR ${MessageTable.IS_CALL_TYPE_CLAUSE}
|
||||
(
|
||||
${MessageTable.EXPIRE_STARTED} = 0
|
||||
OR
|
||||
(${MessageTable.EXPIRES_IN} > 0 AND (${MessageTable.EXPIRE_STARTED} + ${MessageTable.EXPIRES_IN}) > $backupTime + ${TimeUnit.DAYS.toMillis(1)})
|
||||
)
|
||||
"""
|
||||
)
|
||||
.orderBy("${MessageTable.DATE_RECEIVED} ASC")
|
||||
|
|
|
@ -19,7 +19,7 @@ object ChatItemBackupProcessor {
|
|||
val TAG = Log.tag(ChatItemBackupProcessor::class.java)
|
||||
|
||||
fun export(exportState: ExportState, emitter: BackupFrameEmitter) {
|
||||
SignalDatabase.messages.getMessagesForBackup().use { chatItems ->
|
||||
SignalDatabase.messages.getMessagesForBackup(exportState.backupTime).use { chatItems ->
|
||||
for (chatItem in chatItems) {
|
||||
if (exportState.threadIds.contains(chatItem.chatId)) {
|
||||
emitter.emit(Frame(chatItem = chatItem))
|
||||
|
|
|
@ -0,0 +1,148 @@
|
|||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.backup.v2.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.activity.viewModels
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.signal.core.ui.Buttons
|
||||
import org.signal.core.ui.Dividers
|
||||
import org.signal.core.util.getLength
|
||||
import org.thoughtcrime.securesms.BaseActivity
|
||||
import org.thoughtcrime.securesms.MainActivity
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.jobs.ProfileUploadJob
|
||||
import org.thoughtcrime.securesms.profiles.AvatarHelper
|
||||
import org.thoughtcrime.securesms.profiles.edit.CreateProfileActivity
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.registration.RegistrationUtil
|
||||
|
||||
class MessageBackupsTestRestoreActivity : BaseActivity() {
|
||||
companion object {
|
||||
fun getIntent(context: Context): Intent {
|
||||
return Intent(context, MessageBackupsTestRestoreActivity::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
private val viewModel: MessageBackupsTestRestoreViewModel by viewModels()
|
||||
private lateinit var importFileLauncher: ActivityResultLauncher<Intent>
|
||||
|
||||
private fun onPlaintextClicked() {
|
||||
viewModel.onPlaintextToggled()
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
importFileLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
if (result.resultCode == RESULT_OK) {
|
||||
result.data?.data?.let { uri ->
|
||||
contentResolver.getLength(uri)?.let { length ->
|
||||
viewModel.import(length) { contentResolver.openInputStream(uri)!! }
|
||||
}
|
||||
} ?: Toast.makeText(this, "No URI selected", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
setContent {
|
||||
val state by viewModel.state
|
||||
Surface {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
StateLabel(text = "Plaintext?")
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Switch(
|
||||
checked = state.plaintext,
|
||||
onCheckedChange = { onPlaintextClicked() }
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Buttons.LargePrimary(
|
||||
onClick = {
|
||||
val intent = Intent().apply {
|
||||
action = Intent.ACTION_GET_CONTENT
|
||||
type = "application/octet-stream"
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
}
|
||||
|
||||
importFileLauncher.launch(intent)
|
||||
},
|
||||
enabled = !state.importState.inProgress
|
||||
) {
|
||||
Text("Import from file")
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Dividers.Default()
|
||||
|
||||
Buttons.LargeTonal(
|
||||
onClick = { continueRegistration() },
|
||||
enabled = !state.importState.inProgress
|
||||
) {
|
||||
Text("Continue Reg Flow")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun continueRegistration() {
|
||||
if (Recipient.self().profileName.isEmpty || !AvatarHelper.hasAvatar(this, Recipient.self().id)) {
|
||||
val main = MainActivity.clearTop(this)
|
||||
val profile = CreateProfileActivity.getIntentForUserProfile(this)
|
||||
profile.putExtra("next_intent", main)
|
||||
startActivity(profile)
|
||||
} else {
|
||||
RegistrationUtil.maybeMarkRegistrationComplete()
|
||||
ApplicationDependencies.getJobManager().add(ProfileUploadJob())
|
||||
startActivity(MainActivity.clearTop(this))
|
||||
}
|
||||
finish()
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StateLabel(text: String) {
|
||||
Text(
|
||||
text = text,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.backup.v2.ui
|
||||
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.lifecycle.ViewModel
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKey
|
||||
import org.thoughtcrime.securesms.backup.v2.BackupRepository
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import java.io.InputStream
|
||||
|
||||
class MessageBackupsTestRestoreViewModel : ViewModel() {
|
||||
val disposables = CompositeDisposable()
|
||||
|
||||
private val _state: MutableState<ScreenState> = mutableStateOf(ScreenState(importState = ImportState.NONE, plaintext = false))
|
||||
val state: State<ScreenState> = _state
|
||||
|
||||
fun import(length: Long, inputStreamFactory: () -> InputStream) {
|
||||
_state.value = _state.value.copy(importState = ImportState.IN_PROGRESS)
|
||||
|
||||
val self = Recipient.self()
|
||||
val selfData = BackupRepository.SelfData(self.aci.get(), self.pni.get(), self.e164.get(), ProfileKey(self.profileKey))
|
||||
|
||||
disposables += Single.fromCallable { BackupRepository.import(length, inputStreamFactory, selfData, plaintext = _state.value.plaintext) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeBy {
|
||||
_state.value = _state.value.copy(importState = ImportState.NONE)
|
||||
}
|
||||
}
|
||||
|
||||
fun onPlaintextToggled() {
|
||||
_state.value = _state.value.copy(plaintext = !_state.value.plaintext)
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
disposables.clear()
|
||||
}
|
||||
|
||||
data class ScreenState(
|
||||
val importState: ImportState,
|
||||
val plaintext: Boolean
|
||||
)
|
||||
|
||||
enum class ImportState(val inProgress: Boolean = false) {
|
||||
NONE, IN_PROGRESS(true)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.backup.v2.ui
|
||||
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
/**
|
||||
* Represents a "Feature" included for a specify tier of message backups
|
||||
*/
|
||||
data class MessageBackupsTypeFeature(
|
||||
val iconResourceId: Int,
|
||||
val label: String
|
||||
)
|
||||
|
||||
/**
|
||||
* Renders a "feature row" for a given feature.
|
||||
*/
|
||||
@Composable
|
||||
fun MessageBackupsTypeFeatureRow(
|
||||
messageBackupsTypeFeature: MessageBackupsTypeFeature,
|
||||
iconTint: Color = LocalContentColor.current,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = modifier.fillMaxWidth()
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = messageBackupsTypeFeature.iconResourceId),
|
||||
contentDescription = null,
|
||||
tint = iconTint,
|
||||
modifier = Modifier.padding(end = 8.dp)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = messageBackupsTypeFeature.label,
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
}
|
||||
}
|
|
@ -10,7 +10,6 @@ import androidx.compose.foundation.border
|
|||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
|
@ -19,7 +18,6 @@ import androidx.compose.foundation.lazy.LazyColumn
|
|||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.ClickableText
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
|
@ -271,32 +269,8 @@ private fun formatCostPerMonth(pricePerMonth: FiatMoney): String {
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MessageBackupsTypeFeatureRow(messageBackupsTypeFeature: MessageBackupsTypeFeature) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = messageBackupsTypeFeature.iconResourceId),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.padding(end = 8.dp)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = messageBackupsTypeFeature.label,
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class MessageBackupsType(
|
||||
val pricePerMonth: FiatMoney,
|
||||
val title: String,
|
||||
val features: ImmutableList<MessageBackupsTypeFeature>
|
||||
)
|
||||
|
||||
data class MessageBackupsTypeFeature(
|
||||
val iconResourceId: Int,
|
||||
val label: String
|
||||
)
|
||||
|
|
|
@ -0,0 +1,179 @@
|
|||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.backup.v2.ui.restore
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.dimensionResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import org.signal.core.ui.Buttons
|
||||
import org.signal.core.ui.Previews
|
||||
import org.signal.core.ui.theme.SignalTheme
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.MessageBackupsTypeFeature
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.MessageBackupsTypeFeatureRow
|
||||
import org.thoughtcrime.securesms.compose.ComposeFragment
|
||||
import org.thoughtcrime.securesms.devicetransfer.moreoptions.MoreTransferOrRestoreOptionsMode
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
|
||||
/**
|
||||
* Fragment which facilitates restoring from a backup during
|
||||
* registration.
|
||||
*/
|
||||
class RestoreFromBackupFragment : ComposeFragment() {
|
||||
|
||||
private val navArgs: RestoreFromBackupFragmentArgs by navArgs()
|
||||
|
||||
@Composable
|
||||
override fun FragmentContent() {
|
||||
RestoreFromBackupContent(
|
||||
features = persistentListOf(),
|
||||
onRestoreBackupClick = {
|
||||
// TODO [message-backups] Restore backup.
|
||||
},
|
||||
onCancelClick = {
|
||||
findNavController()
|
||||
.popBackStack()
|
||||
},
|
||||
onMoreOptionsClick = {
|
||||
findNavController()
|
||||
.safeNavigate(RestoreFromBackupFragmentDirections.actionRestoreFromBacakupFragmentToMoreOptions(MoreTransferOrRestoreOptionsMode.SELECTION))
|
||||
},
|
||||
cancelable = navArgs.cancelable
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun RestoreFromBackupContentPreview() {
|
||||
Previews.Preview {
|
||||
RestoreFromBackupContent(
|
||||
features = persistentListOf(
|
||||
MessageBackupsTypeFeature(
|
||||
iconResourceId = R.drawable.symbol_thread_compact_bold_16,
|
||||
label = "Your last 30 days of media"
|
||||
),
|
||||
MessageBackupsTypeFeature(
|
||||
iconResourceId = R.drawable.symbol_recent_compact_bold_16,
|
||||
label = "All of your text messages"
|
||||
)
|
||||
),
|
||||
onRestoreBackupClick = {},
|
||||
onCancelClick = {},
|
||||
onMoreOptionsClick = {},
|
||||
true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RestoreFromBackupContent(
|
||||
features: ImmutableList<MessageBackupsTypeFeature>,
|
||||
onRestoreBackupClick: () -> Unit,
|
||||
onCancelClick: () -> Unit,
|
||||
onMoreOptionsClick: () -> Unit,
|
||||
cancelable: Boolean
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter))
|
||||
.padding(top = 40.dp, bottom = 24.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Restore from backup", // TODO [message-backups] Finalized copy.
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
modifier = Modifier.padding(bottom = 12.dp)
|
||||
)
|
||||
|
||||
val yourLastBackupText = buildAnnotatedString {
|
||||
append("Your last backup was made on March 5, 2024 at 9:00am.") // TODO [message-backups] Finalized copy.
|
||||
append(" ")
|
||||
withStyle(SpanStyle(fontWeight = FontWeight.SemiBold)) {
|
||||
append("Only media sent or received in the past 30 days is included.") // TODO [message-backups] Finalized copy.
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
text = yourLastBackupText,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(bottom = 28.dp)
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(color = SignalTheme.colors.colorSurface2, shape = RoundedCornerShape(18.dp))
|
||||
.padding(horizontal = 20.dp)
|
||||
.padding(top = 20.dp, bottom = 18.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Your backup includes:", // TODO [message-backups] Finalized copy.
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
modifier = Modifier.padding(bottom = 6.dp)
|
||||
)
|
||||
|
||||
features.forEach {
|
||||
MessageBackupsTypeFeatureRow(
|
||||
messageBackupsTypeFeature = it,
|
||||
iconTint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.padding(start = 16.dp, top = 6.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
Buttons.LargeTonal(
|
||||
onClick = onRestoreBackupClick,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
text = "Restore backup" // TODO [message-backups] Finalized copy.
|
||||
)
|
||||
}
|
||||
|
||||
if (cancelable) {
|
||||
TextButton(
|
||||
onClick = onCancelClick,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = android.R.string.cancel)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
TextButton(
|
||||
onClick = onMoreOptionsClick,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.TransferOrRestoreFragment__more_options)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -182,7 +182,7 @@ class CallLogAdapter(
|
|||
binding: CallLogAdapterItemBinding,
|
||||
private val onCallLinkClicked: (CallLogRow.CallLink) -> Unit,
|
||||
private val onCallLinkLongClicked: (View, CallLogRow.CallLink) -> Boolean,
|
||||
private val onStartVideoCallClicked: (Recipient) -> Unit
|
||||
private val onStartVideoCallClicked: (Recipient, Boolean) -> Unit
|
||||
) : BindingViewHolder<CallLinkModel, CallLogAdapterItemBinding>(binding) {
|
||||
override fun bind(model: CallLinkModel) {
|
||||
if (payload.size == 1 && payload.contains(PAYLOAD_TIMESTAMP)) {
|
||||
|
@ -231,7 +231,7 @@ class CallLogAdapter(
|
|||
binding.callType.setImageResource(R.drawable.symbol_video_24)
|
||||
binding.callType.contentDescription = context.getString(R.string.CallLogAdapter__start_a_video_call)
|
||||
binding.callType.setOnClickListener {
|
||||
onStartVideoCallClicked(model.callLink.recipient)
|
||||
onStartVideoCallClicked(model.callLink.recipient, true)
|
||||
}
|
||||
binding.callType.visible = true
|
||||
binding.groupCallButton.visible = false
|
||||
|
@ -243,7 +243,7 @@ class CallLogAdapter(
|
|||
private val onCallClicked: (CallLogRow.Call) -> Unit,
|
||||
private val onCallLongClicked: (View, CallLogRow.Call) -> Boolean,
|
||||
private val onStartAudioCallClicked: (Recipient) -> Unit,
|
||||
private val onStartVideoCallClicked: (Recipient) -> Unit
|
||||
private val onStartVideoCallClicked: (Recipient, Boolean) -> Unit
|
||||
) : BindingViewHolder<CallModel, CallLogAdapterItemBinding>(binding) {
|
||||
override fun bind(model: CallModel) {
|
||||
itemView.setOnClickListener {
|
||||
|
@ -333,7 +333,7 @@ class CallLogAdapter(
|
|||
CallTable.Type.VIDEO_CALL -> {
|
||||
binding.callType.setImageResource(R.drawable.symbol_video_24)
|
||||
binding.callType.contentDescription = context.getString(R.string.CallLogAdapter__start_a_video_call)
|
||||
binding.callType.setOnClickListener { onStartVideoCallClicked(model.call.peer) }
|
||||
binding.callType.setOnClickListener { onStartVideoCallClicked(model.call.peer, true) }
|
||||
binding.callType.visible = true
|
||||
binding.groupCallButton.visible = false
|
||||
}
|
||||
|
@ -341,8 +341,8 @@ class CallLogAdapter(
|
|||
CallTable.Type.GROUP_CALL, CallTable.Type.AD_HOC_CALL -> {
|
||||
binding.callType.setImageResource(R.drawable.symbol_video_24)
|
||||
binding.callType.contentDescription = context.getString(R.string.CallLogAdapter__start_a_video_call)
|
||||
binding.callType.setOnClickListener { onStartVideoCallClicked(model.call.peer) }
|
||||
binding.groupCallButton.setOnClickListener { onStartVideoCallClicked(model.call.peer) }
|
||||
binding.callType.setOnClickListener { onStartVideoCallClicked(model.call.peer, model.call.canUserBeginCall) }
|
||||
binding.groupCallButton.setOnClickListener { onStartVideoCallClicked(model.call.peer, model.call.canUserBeginCall) }
|
||||
|
||||
when (model.call.groupCallState) {
|
||||
CallLogRow.GroupCallState.NONE, CallLogRow.GroupCallState.FULL -> {
|
||||
|
@ -472,6 +472,6 @@ class CallLogAdapter(
|
|||
/**
|
||||
* Invoked when user presses the video icon
|
||||
*/
|
||||
fun onStartVideoCallClicked(recipient: Recipient)
|
||||
fun onStartVideoCallClicked(recipient: Recipient, canUserBeginCall: Boolean)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ import androidx.fragment.app.Fragment
|
|||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.transition.TransitionInflater
|
||||
import com.google.android.material.appbar.AppBarLayout
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
|
@ -32,6 +33,7 @@ import org.signal.core.util.DimensionUnit
|
|||
import org.signal.core.util.concurrent.LifecycleDisposable
|
||||
import org.signal.core.util.concurrent.addTo
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.MainActivity
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.calls.links.details.CallLinkDetailsActivity
|
||||
import org.thoughtcrime.securesms.calls.new.NewCallActivity
|
||||
|
@ -45,6 +47,7 @@ import org.thoughtcrime.securesms.components.settings.app.notifications.manual.N
|
|||
import org.thoughtcrime.securesms.components.settings.conversation.ConversationSettingsActivity
|
||||
import org.thoughtcrime.securesms.conversation.ConversationUpdateTick
|
||||
import org.thoughtcrime.securesms.conversation.SignalBottomActionBarController
|
||||
import org.thoughtcrime.securesms.conversation.v2.ConversationDialogs
|
||||
import org.thoughtcrime.securesms.conversationlist.ConversationFilterBehavior
|
||||
import org.thoughtcrime.securesms.conversationlist.chatfilter.ConversationFilterSource
|
||||
import org.thoughtcrime.securesms.conversationlist.chatfilter.ConversationListFilterPullView.OnCloseClicked
|
||||
|
@ -123,6 +126,12 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal
|
|||
val callLogAdapter = CallLogAdapter(this)
|
||||
disposables.bindTo(viewLifecycleOwner)
|
||||
callLogAdapter.setPagingController(viewModel.controller)
|
||||
callLogAdapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
|
||||
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
|
||||
(requireActivity() as? MainActivity)?.onFirstRender()
|
||||
callLogAdapter.unregisterAdapterDataObserver(this)
|
||||
}
|
||||
})
|
||||
|
||||
val scrollToPositionDelegate = ScrollToPositionDelegate(
|
||||
recyclerView = binding.recycler,
|
||||
|
@ -376,8 +385,12 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal
|
|||
CommunicationActions.startVoiceCall(this, recipient)
|
||||
}
|
||||
|
||||
override fun onStartVideoCallClicked(recipient: Recipient) {
|
||||
CommunicationActions.startVideoCall(this, recipient)
|
||||
override fun onStartVideoCallClicked(recipient: Recipient, canUserBeginCall: Boolean) {
|
||||
if (canUserBeginCall) {
|
||||
CommunicationActions.startVideoCall(this, recipient)
|
||||
} else {
|
||||
ConversationDialogs.displayCannotStartGroupCallDueToPermissionsDialog(requireContext())
|
||||
}
|
||||
}
|
||||
|
||||
override fun startSelection(call: CallLogRow) {
|
||||
|
|
|
@ -43,8 +43,9 @@ class CallLogRepository(
|
|||
|
||||
fun markAllCallEventsRead() {
|
||||
SignalExecutors.BOUNDED_IO.execute {
|
||||
val latestCall = SignalDatabase.calls.getLatestCall() ?: return@execute
|
||||
SignalDatabase.calls.markAllCallEventsRead()
|
||||
ApplicationDependencies.getJobManager().add(CallLogEventSendJob.forMarkedAsRead(System.currentTimeMillis()))
|
||||
ApplicationDependencies.getJobManager().add(CallLogEventSendJob.forMarkedAsRead(latestCall))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -95,10 +96,10 @@ class CallLogRepository(
|
|||
fun deleteAllCallLogsOnOrBeforeNow(): Single<Int> {
|
||||
return Single.fromCallable {
|
||||
SignalDatabase.rawDatabase.withinTransaction {
|
||||
val latestTimestamp = SignalDatabase.calls.getLatestTimestamp()
|
||||
SignalDatabase.calls.deleteNonAdHocCallEventsOnOrBefore(latestTimestamp)
|
||||
SignalDatabase.callLinks.deleteNonAdminCallLinksOnOrBefore(latestTimestamp)
|
||||
ApplicationDependencies.getJobManager().add(CallLogEventSendJob.forClearHistory(latestTimestamp))
|
||||
val latestCall = SignalDatabase.calls.getLatestCall() ?: return@withinTransaction
|
||||
SignalDatabase.calls.deleteNonAdHocCallEventsOnOrBefore(latestCall.timestamp)
|
||||
SignalDatabase.callLinks.deleteNonAdminCallLinksOnOrBefore(latestCall.timestamp)
|
||||
ApplicationDependencies.getJobManager().add(CallLogEventSendJob.forClearHistory(latestCall))
|
||||
}
|
||||
|
||||
SignalDatabase.callLinks.getAllAdminCallLinksExcept(emptySet())
|
||||
|
|
|
@ -41,6 +41,7 @@ sealed class CallLogRow {
|
|||
val children: Set<Long>,
|
||||
val searchQuery: String?,
|
||||
val callLinkPeekInfo: CallLinkPeekInfo?,
|
||||
val canUserBeginCall: Boolean,
|
||||
override val id: Id = Id.Call(children)
|
||||
) : CallLogRow()
|
||||
|
||||
|
|
|
@ -130,9 +130,9 @@ open class InsetAwareConstraintLayout @JvmOverloads constructor(
|
|||
|
||||
if (previousKeyboardHeight != keyboardInsets.bottom) {
|
||||
keyboardStateListeners.forEach {
|
||||
if (previousKeyboardHeight <= 0) {
|
||||
if (previousKeyboardHeight <= 0 && keyboardInsets.bottom > 0) {
|
||||
it.onKeyboardShown()
|
||||
} else {
|
||||
} else if (previousKeyboardHeight > 0 && keyboardInsets.bottom <= 0) {
|
||||
it.onKeyboardHidden()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
package org.thoughtcrime.securesms.components.reminder;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.registration.RegistrationNavigationActivity;
|
||||
|
||||
public class PushRegistrationReminder extends Reminder {
|
||||
|
||||
public PushRegistrationReminder(final Context context) {
|
||||
super(R.string.reminder_header_push_title, R.string.reminder_header_push_text);
|
||||
|
||||
setOkListener(v -> context.startActivity(RegistrationNavigationActivity.newIntentForReRegistration(context)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDismissable() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public static boolean isEligible() {
|
||||
return !SignalStore.account().isRegistered();
|
||||
}
|
||||
}
|
|
@ -5,6 +5,7 @@ import android.content.Context;
|
|||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.registration.RegistrationNavigationActivity;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
|
@ -26,6 +27,6 @@ public class UnauthorizedReminder extends Reminder {
|
|||
}
|
||||
|
||||
public static boolean isEligible(Context context) {
|
||||
return TextSecurePreferences.isUnauthorizedReceived(context);
|
||||
return TextSecurePreferences.isUnauthorizedReceived(context) || !SignalStore.account().isRegistered();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -152,7 +152,7 @@ class AppSettingsFragment : DSLSettingsFragment(
|
|||
onClick = {
|
||||
findNavController().safeNavigate(R.id.action_appSettingsFragment_to_deviceActivity)
|
||||
},
|
||||
isEnabled = state.isDeprecatedOrUnregistered()
|
||||
isEnabled = state.isRegisteredAndUpToDate()
|
||||
)
|
||||
|
||||
externalLinkPref(
|
||||
|
@ -177,7 +177,7 @@ class AppSettingsFragment : DSLSettingsFragment(
|
|||
onClick = {
|
||||
findNavController().safeNavigate(R.id.action_appSettingsFragment_to_chatsSettingsFragment)
|
||||
},
|
||||
isEnabled = state.isDeprecatedOrUnregistered()
|
||||
isEnabled = state.isRegisteredAndUpToDate()
|
||||
)
|
||||
|
||||
clickPref(
|
||||
|
@ -186,7 +186,7 @@ class AppSettingsFragment : DSLSettingsFragment(
|
|||
onClick = {
|
||||
findNavController().safeNavigate(AppSettingsFragmentDirections.actionAppSettingsFragmentToStoryPrivacySettings(R.string.preferences__stories))
|
||||
},
|
||||
isEnabled = state.isDeprecatedOrUnregistered()
|
||||
isEnabled = state.isRegisteredAndUpToDate()
|
||||
)
|
||||
|
||||
clickPref(
|
||||
|
@ -195,7 +195,7 @@ class AppSettingsFragment : DSLSettingsFragment(
|
|||
onClick = {
|
||||
findNavController().safeNavigate(R.id.action_appSettingsFragment_to_notificationsSettingsFragment)
|
||||
},
|
||||
isEnabled = state.isDeprecatedOrUnregistered()
|
||||
isEnabled = state.isRegisteredAndUpToDate()
|
||||
)
|
||||
|
||||
clickPref(
|
||||
|
@ -204,7 +204,7 @@ class AppSettingsFragment : DSLSettingsFragment(
|
|||
onClick = {
|
||||
findNavController().safeNavigate(R.id.action_appSettingsFragment_to_privacySettingsFragment)
|
||||
},
|
||||
isEnabled = state.isDeprecatedOrUnregistered()
|
||||
isEnabled = state.isRegisteredAndUpToDate()
|
||||
)
|
||||
|
||||
clickPref(
|
||||
|
|
|
@ -8,7 +8,7 @@ data class AppSettingsState(
|
|||
val userUnregistered: Boolean,
|
||||
val clientDeprecated: Boolean
|
||||
) {
|
||||
fun isDeprecatedOrUnregistered(): Boolean {
|
||||
return !(userUnregistered || clientDeprecated)
|
||||
fun isRegisteredAndUpToDate(): Boolean {
|
||||
return !userUnregistered && !clientDeprecated
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ class AppSettingsViewModel : ViewModel() {
|
|||
AppSettingsState(
|
||||
Recipient.self(),
|
||||
0,
|
||||
TextSecurePreferences.isUnauthorizedReceived(ApplicationDependencies.getApplication()),
|
||||
TextSecurePreferences.isUnauthorizedReceived(ApplicationDependencies.getApplication()) || !SignalStore.account().isRegistered,
|
||||
SignalStore.misc().isClientDeprecated
|
||||
)
|
||||
)
|
||||
|
@ -37,6 +37,11 @@ class AppSettingsViewModel : ViewModel() {
|
|||
}
|
||||
|
||||
fun refreshDeprecatedOrUnregistered() {
|
||||
store.update { it.copy(clientDeprecated = SignalStore.misc().isClientDeprecated, userUnregistered = TextSecurePreferences.isUnauthorizedReceived(ApplicationDependencies.getApplication())) }
|
||||
store.update {
|
||||
it.copy(
|
||||
clientDeprecated = SignalStore.misc().isClientDeprecated,
|
||||
userUnregistered = TextSecurePreferences.isUnauthorizedReceived(ApplicationDependencies.getApplication()) || !SignalStore.account().isRegistered
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -291,7 +291,7 @@ class ChangeNumberRepository(
|
|||
true
|
||||
)
|
||||
|
||||
SignalStore.misc().setPniInitializedDevices(true)
|
||||
SignalStore.misc().hasPniInitializedDevices = true
|
||||
ApplicationDependencies.getGroupsV2Authorization().clear()
|
||||
}
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ import androidx.navigation.fragment.findNavController
|
|||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import org.signal.core.util.AppUtil
|
||||
import org.signal.core.util.ThreadUtil
|
||||
import org.signal.core.util.concurrent.SignalExecutors
|
||||
import org.signal.core.util.concurrent.SimpleTask
|
||||
import org.signal.core.util.logging.Log
|
||||
|
@ -24,6 +25,7 @@ import org.thoughtcrime.securesms.R
|
|||
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||
import org.thoughtcrime.securesms.components.settings.app.privacy.advanced.AdvancedPrivacySettingsRepository
|
||||
import org.thoughtcrime.securesms.components.settings.configure
|
||||
import org.thoughtcrime.securesms.database.JobDatabase
|
||||
import org.thoughtcrime.securesms.database.LocalMetricsDatabase
|
||||
|
@ -137,6 +139,14 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
|
|||
}
|
||||
)
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from("Unregister"),
|
||||
summary = DSLSettingsText.from("This will unregister your account without deleting it."),
|
||||
onClick = {
|
||||
onUnregisterClicked()
|
||||
}
|
||||
)
|
||||
|
||||
dividerPref()
|
||||
|
||||
sectionHeaderPref(DSLSettingsText.from("Miscellaneous"))
|
||||
|
@ -714,6 +724,32 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
|
|||
}
|
||||
}
|
||||
|
||||
private fun onUnregisterClicked() {
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle("Unregister?")
|
||||
.setMessage("Are you sure? You'll have to re-register to use Signal again -- no promises that the process will go smoothly.")
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
AdvancedPrivacySettingsRepository(requireContext()).disablePushMessages {
|
||||
ThreadUtil.runOnMain {
|
||||
when (it) {
|
||||
AdvancedPrivacySettingsRepository.DisablePushMessagesResult.SUCCESS -> {
|
||||
SignalStore.account().setRegistered(false)
|
||||
SignalStore.registrationValues().clearRegistrationComplete()
|
||||
SignalStore.registrationValues().clearHasUploadedProfile()
|
||||
Toast.makeText(context, "Unregistered!", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
AdvancedPrivacySettingsRepository.DisablePushMessagesResult.NETWORK_ERROR -> {
|
||||
Toast.makeText(context, "Network error!", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun copyPaymentsDataToClipboard() {
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setMessage(
|
||||
|
|
|
@ -61,7 +61,7 @@ class InternalSettingsViewModel(private val repository: InternalSettingsReposito
|
|||
}
|
||||
|
||||
fun resetPnpInitializedState() {
|
||||
SignalStore.misc().setPniInitializedDevices(false)
|
||||
SignalStore.misc().hasPniInitializedDevices = false
|
||||
refresh()
|
||||
}
|
||||
|
||||
|
@ -140,7 +140,7 @@ class InternalSettingsViewModel(private val repository: InternalSettingsReposito
|
|||
delayResends = SignalStore.internalValues().delayResends(),
|
||||
disableStorageService = SignalStore.internalValues().storageServiceDisabled(),
|
||||
canClearOnboardingState = SignalStore.storyValues().hasDownloadedOnboardingStory && Stories.isFeatureEnabled(),
|
||||
pnpInitialized = SignalStore.misc().hasPniInitializedDevices(),
|
||||
pnpInitialized = SignalStore.misc().hasPniInitializedDevices,
|
||||
useConversationItemV2ForMedia = SignalStore.internalValues().useConversationItemV2Media(),
|
||||
)
|
||||
|
||||
|
|
|
@ -6,15 +6,11 @@ import android.content.Intent
|
|||
import android.content.IntentFilter
|
||||
import android.graphics.PorterDuff
|
||||
import android.graphics.PorterDuffColorFilter
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.net.ConnectivityManager
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.widget.TextViewCompat
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.SignalProgressDialog
|
||||
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
||||
|
@ -23,7 +19,6 @@ import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
|||
import org.thoughtcrime.securesms.components.settings.configure
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter
|
||||
import org.thoughtcrime.securesms.registration.RegistrationNavigationActivity
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions
|
||||
import org.thoughtcrime.securesms.util.SecurePreferenceManager
|
||||
import org.thoughtcrime.securesms.util.SpanUtil
|
||||
|
@ -107,40 +102,6 @@ class AdvancedPrivacySettingsFragment : DSLSettingsFragment(R.string.preferences
|
|||
|
||||
private fun getConfiguration(state: AdvancedPrivacySettingsState): DSLConfiguration {
|
||||
return configure {
|
||||
switchPref(
|
||||
title = DSLSettingsText.from(R.string.preferences__signal_messages_and_calls),
|
||||
summary = DSLSettingsText.from(getPushToggleSummary(state.isPushEnabled)),
|
||||
isChecked = state.isPushEnabled
|
||||
) {
|
||||
if (state.isPushEnabled) {
|
||||
val builder = MaterialAlertDialogBuilder(requireContext()).apply {
|
||||
setMessage(R.string.ApplicationPreferencesActivity_disable_signal_messages_and_calls_by_unregistering)
|
||||
setNegativeButton(android.R.string.cancel, null)
|
||||
setPositiveButton(
|
||||
android.R.string.ok
|
||||
) { _, _ -> viewModel.disablePushMessages() }
|
||||
}
|
||||
|
||||
val icon: Drawable = requireNotNull(ContextCompat.getDrawable(builder.context, R.drawable.symbol_info_24))
|
||||
icon.setBounds(0, 0, ViewUtil.dpToPx(32), ViewUtil.dpToPx(32))
|
||||
|
||||
val title = TextView(builder.context)
|
||||
val padding = ViewUtil.dpToPx(16)
|
||||
title.setText(R.string.ApplicationPreferencesActivity_disable_signal_messages_and_calls)
|
||||
title.setPadding(padding, padding, padding, padding)
|
||||
title.compoundDrawablePadding = padding / 2
|
||||
TextViewCompat.setTextAppearance(title, R.style.TextAppearance_Signal_Title2_MaterialDialog)
|
||||
TextViewCompat.setCompoundDrawablesRelative(title, icon, null, null, null)
|
||||
|
||||
builder
|
||||
.setCustomTitle(title)
|
||||
.setOnDismissListener { viewModel.refresh() }
|
||||
.show()
|
||||
} else {
|
||||
startActivity(RegistrationNavigationActivity.newIntentForReRegistration(requireContext()))
|
||||
}
|
||||
}
|
||||
|
||||
switchPref(
|
||||
title = DSLSettingsText.from(R.string.preferences_advanced__always_relay_calls),
|
||||
summary = DSLSettingsText.from(R.string.preferences_advanced__relay_all_calls_through_the_signal_server_to_avoid_revealing_your_ip_address),
|
||||
|
|
|
@ -38,25 +38,6 @@ class AdvancedPrivacySettingsViewModel(
|
|||
)
|
||||
}
|
||||
|
||||
fun disablePushMessages() {
|
||||
store.update { getState().copy(showProgressSpinner = true) }
|
||||
|
||||
repository.disablePushMessages {
|
||||
when (it) {
|
||||
AdvancedPrivacySettingsRepository.DisablePushMessagesResult.SUCCESS -> {
|
||||
SignalStore.account().setRegistered(false)
|
||||
SignalStore.registrationValues().clearRegistrationComplete()
|
||||
SignalStore.registrationValues().clearHasUploadedProfile()
|
||||
}
|
||||
AdvancedPrivacySettingsRepository.DisablePushMessagesResult.NETWORK_ERROR -> {
|
||||
singleEvents.postValue(Event.DISABLE_PUSH_FAILED)
|
||||
}
|
||||
}
|
||||
|
||||
store.update { getState().copy(showProgressSpinner = false) }
|
||||
}
|
||||
}
|
||||
|
||||
fun setAlwaysRelayCalls(enabled: Boolean) {
|
||||
sharedPreferences.edit().putBoolean(TextSecurePreferences.ALWAYS_RELAY_CALLS_PREF, enabled).apply()
|
||||
refresh()
|
||||
|
|
|
@ -14,6 +14,7 @@ import android.view.ViewGroup
|
|||
import android.widget.FrameLayout
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.doOnPreDraw
|
||||
|
@ -76,6 +77,7 @@ import org.thoughtcrime.securesms.groups.ui.managegroup.dialogs.GroupsLearnMoreB
|
|||
import org.thoughtcrime.securesms.mediaoverview.MediaOverviewActivity
|
||||
import org.thoughtcrime.securesms.mediapreview.MediaIntentFactory
|
||||
import org.thoughtcrime.securesms.messagerequests.MessageRequestRepository
|
||||
import org.thoughtcrime.securesms.nicknames.NicknameActivity
|
||||
import org.thoughtcrime.securesms.profiles.edit.CreateProfileActivity
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientExporter
|
||||
|
@ -92,6 +94,7 @@ import org.thoughtcrime.securesms.util.CommunicationActions
|
|||
import org.thoughtcrime.securesms.util.ContextUtil
|
||||
import org.thoughtcrime.securesms.util.DateUtils
|
||||
import org.thoughtcrime.securesms.util.ExpirationUtil
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||
import org.thoughtcrime.securesms.util.Material3OnScrollHelper
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||
|
@ -120,10 +123,6 @@ class ConversationSettingsFragment : DSLSettingsFragment(
|
|||
}
|
||||
}
|
||||
|
||||
private val unblockIcon by lazy {
|
||||
ContextUtil.requireDrawable(requireContext(), R.drawable.symbol_block_24)
|
||||
}
|
||||
|
||||
private val leaveIcon by lazy {
|
||||
ContextUtil.requireDrawable(requireContext(), R.drawable.symbol_leave_24).apply {
|
||||
colorFilter = PorterDuffColorFilter(alertTint, PorterDuff.Mode.SRC_IN)
|
||||
|
@ -153,6 +152,7 @@ class ConversationSettingsFragment : DSLSettingsFragment(
|
|||
private lateinit var toolbarTitle: TextView
|
||||
private lateinit var toolbarBackground: View
|
||||
private lateinit var addToGroupStoryDelegate: AddToGroupStoryDelegate
|
||||
private lateinit var nicknameLauncher: ActivityResultLauncher<NicknameActivity.Args>
|
||||
|
||||
private val navController get() = Navigation.findNavController(requireView())
|
||||
private val lifecycleDisposable = LifecycleDisposable()
|
||||
|
@ -220,6 +220,10 @@ class ConversationSettingsFragment : DSLSettingsFragment(
|
|||
}
|
||||
|
||||
override fun bindAdapter(adapter: MappingAdapter) {
|
||||
nicknameLauncher = registerForActivityResult(NicknameActivity.Contract()) {
|
||||
// Intentionally left blank
|
||||
}
|
||||
|
||||
val args = ConversationSettingsFragmentArgs.fromBundle(requireArguments())
|
||||
|
||||
BioTextPreference.register(adapter)
|
||||
|
@ -467,9 +471,9 @@ class ConversationSettingsFragment : DSLSettingsFragment(
|
|||
|
||||
val summary = DSLSettingsText.from(formatDisappearingMessagesLifespan(state.disappearingMessagesLifespan))
|
||||
val icon = if (state.disappearingMessagesLifespan <= 0 || state.recipient.isBlocked) {
|
||||
R.drawable.ic_update_timer_disabled_16
|
||||
R.drawable.symbol_timer_slash_24
|
||||
} else {
|
||||
R.drawable.ic_update_timer_16
|
||||
R.drawable.symbol_timer_24
|
||||
}
|
||||
|
||||
var enabled = !state.recipient.isBlocked
|
||||
|
@ -494,10 +498,25 @@ class ConversationSettingsFragment : DSLSettingsFragment(
|
|||
)
|
||||
}
|
||||
|
||||
if (FeatureFlags.nicknames() && state.recipient.isIndividual && !state.recipient.isSelf) {
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.NicknameActivity__nickname),
|
||||
icon = DSLSettingsIcon.from(R.drawable.symbol_edit_24),
|
||||
onClick = {
|
||||
nicknameLauncher.launch(
|
||||
NicknameActivity.Args(
|
||||
state.recipient.id,
|
||||
false
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (!state.recipient.isReleaseNotes) {
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.preferences__chat_color_and_wallpaper),
|
||||
icon = DSLSettingsIcon.from(R.drawable.ic_color_24),
|
||||
icon = DSLSettingsIcon.from(R.drawable.symbol_color_24),
|
||||
onClick = {
|
||||
startActivity(ChatWallpaperActivity.createIntent(requireContext(), state.recipient.id))
|
||||
}
|
||||
|
@ -507,7 +526,7 @@ class ConversationSettingsFragment : DSLSettingsFragment(
|
|||
if (!state.recipient.isSelf) {
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.ConversationSettingsFragment__sounds_and_notifications),
|
||||
icon = DSLSettingsIcon.from(R.drawable.ic_speaker_24),
|
||||
icon = DSLSettingsIcon.from(R.drawable.symbol_speaker_24),
|
||||
isEnabled = !state.isDeprecatedOrUnregistered,
|
||||
onClick = {
|
||||
val action = ConversationSettingsFragmentDirections.actionConversationSettingsFragmentToSoundsAndNotificationsSettingsFragment(state.recipient.id)
|
||||
|
@ -552,7 +571,7 @@ class ConversationSettingsFragment : DSLSettingsFragment(
|
|||
if (!state.recipient.isReleaseNotes && !state.recipient.isSelf) {
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.ConversationSettingsFragment__view_safety_number),
|
||||
icon = DSLSettingsIcon.from(R.drawable.ic_safety_number_24),
|
||||
icon = DSLSettingsIcon.from(R.drawable.symbol_safety_number_24),
|
||||
isEnabled = !state.isDeprecatedOrUnregistered,
|
||||
onClick = {
|
||||
VerifyIdentityActivity.startOrShowExchangeMessagesDialog(requireActivity(), recipientState.identityRecord)
|
||||
|
@ -779,11 +798,10 @@ class ConversationSettingsFragment : DSLSettingsFragment(
|
|||
}
|
||||
|
||||
val titleTint = if (isBlocked) null else if (state.isDeprecatedOrUnregistered) alertDisabledTint else alertTint
|
||||
val blockUnblockIcon = if (isBlocked) unblockIcon else blockIcon
|
||||
|
||||
clickPref(
|
||||
title = if (titleTint != null) DSLSettingsText.from(title, titleTint) else DSLSettingsText.from(title),
|
||||
icon = DSLSettingsIcon.from(blockUnblockIcon),
|
||||
icon = if (isBlocked) DSLSettingsIcon.from(R.drawable.symbol_block_24) else DSLSettingsIcon.from(blockIcon),
|
||||
isEnabled = !state.isDeprecatedOrUnregistered,
|
||||
onClick = {
|
||||
if (state.recipient.isBlocked) {
|
||||
|
|
|
@ -61,6 +61,7 @@ public class CallParticipantView extends ConstraintLayout {
|
|||
private boolean infoMode;
|
||||
private boolean raiseHandAllowed;
|
||||
private Runnable missingMediaKeysUpdater;
|
||||
private boolean shouldRenderInPip;
|
||||
|
||||
private SelfPipMode selfPipMode = SelfPipMode.NOT_SELF_PIP;
|
||||
|
||||
|
@ -198,6 +199,8 @@ public class CallParticipantView extends ConstraintLayout {
|
|||
pipBadge.setBadgeFromRecipient(participant.getRecipient());
|
||||
contactPhoto = participant.getRecipient().getContactPhoto();
|
||||
}
|
||||
|
||||
setRenderInPip(shouldRenderInPip);
|
||||
}
|
||||
|
||||
private boolean isMissingMediaKeys(@NonNull CallParticipant participant) {
|
||||
|
@ -223,10 +226,15 @@ public class CallParticipantView extends ConstraintLayout {
|
|||
}
|
||||
|
||||
void setRenderInPip(boolean shouldRenderInPip) {
|
||||
this.shouldRenderInPip = shouldRenderInPip;
|
||||
|
||||
if (infoMode) {
|
||||
infoMessage.setVisibility(shouldRenderInPip ? View.GONE : View.VISIBLE);
|
||||
infoMoreInfo.setVisibility(shouldRenderInPip ? View.GONE : View.VISIBLE);
|
||||
infoOverlay.setOnClickListener(shouldRenderInPip ? v -> infoMoreInfo.performClick() : null);
|
||||
return;
|
||||
} else {
|
||||
infoOverlay.setOnClickListener(null);
|
||||
}
|
||||
|
||||
avatar.setVisibility(shouldRenderInPip ? View.GONE : View.VISIBLE);
|
||||
|
@ -243,7 +251,7 @@ public class CallParticipantView extends ConstraintLayout {
|
|||
* Adjust UI elements for the various self PIP positions. If called after a {@link TransitionManager#beginDelayedTransition(ViewGroup, Transition)},
|
||||
* the changes to the UI elements will animate.
|
||||
*/
|
||||
void setSelfPipMode(@NonNull SelfPipMode selfPipMode) {
|
||||
void setSelfPipMode(@NonNull SelfPipMode selfPipMode, boolean isMoreThanOneCameraAvailable) {
|
||||
Preconditions.checkArgument(selfPipMode != SelfPipMode.NOT_SELF_PIP);
|
||||
|
||||
if (this.selfPipMode == selfPipMode) {
|
||||
|
@ -274,26 +282,30 @@ public class CallParticipantView extends ConstraintLayout {
|
|||
ViewUtil.dpToPx(6)
|
||||
);
|
||||
|
||||
constraints.setVisibility(R.id.call_participant_switch_camera, View.VISIBLE);
|
||||
constraints.setMargin(
|
||||
R.id.call_participant_switch_camera,
|
||||
ConstraintSet.END,
|
||||
ViewUtil.dpToPx(6)
|
||||
);
|
||||
constraints.setMargin(
|
||||
R.id.call_participant_switch_camera,
|
||||
ConstraintSet.BOTTOM,
|
||||
ViewUtil.dpToPx(6)
|
||||
);
|
||||
constraints.constrainWidth(R.id.call_participant_switch_camera, ViewUtil.dpToPx(28));
|
||||
constraints.constrainHeight(R.id.call_participant_switch_camera, ViewUtil.dpToPx(28));
|
||||
if (isMoreThanOneCameraAvailable) {
|
||||
constraints.setVisibility(R.id.call_participant_switch_camera, View.VISIBLE);
|
||||
constraints.setMargin(
|
||||
R.id.call_participant_switch_camera,
|
||||
ConstraintSet.END,
|
||||
ViewUtil.dpToPx(6)
|
||||
);
|
||||
constraints.setMargin(
|
||||
R.id.call_participant_switch_camera,
|
||||
ConstraintSet.BOTTOM,
|
||||
ViewUtil.dpToPx(6)
|
||||
);
|
||||
constraints.constrainWidth(R.id.call_participant_switch_camera, ViewUtil.dpToPx(28));
|
||||
constraints.constrainHeight(R.id.call_participant_switch_camera, ViewUtil.dpToPx(28));
|
||||
|
||||
ViewGroup.LayoutParams params = switchCameraIcon.getLayoutParams();
|
||||
params.width = params.height = ViewUtil.dpToPx(16);
|
||||
switchCameraIcon.setLayoutParams(params);
|
||||
ViewGroup.LayoutParams params = switchCameraIcon.getLayoutParams();
|
||||
params.width = params.height = ViewUtil.dpToPx(16);
|
||||
switchCameraIcon.setLayoutParams(params);
|
||||
|
||||
switchCameraIconFrame.setClickable(false);
|
||||
switchCameraIconFrame.setEnabled(false);
|
||||
switchCameraIconFrame.setClickable(false);
|
||||
switchCameraIconFrame.setEnabled(false);
|
||||
} else {
|
||||
constraints.setVisibility(R.id.call_participant_switch_camera, View.GONE);
|
||||
}
|
||||
}
|
||||
case EXPANDED_SELF_PIP -> {
|
||||
constraints.connect(
|
||||
|
@ -313,26 +325,30 @@ public class CallParticipantView extends ConstraintLayout {
|
|||
ViewUtil.dpToPx(8)
|
||||
);
|
||||
|
||||
constraints.setVisibility(R.id.call_participant_switch_camera, View.VISIBLE);
|
||||
constraints.setMargin(
|
||||
R.id.call_participant_switch_camera,
|
||||
ConstraintSet.END,
|
||||
ViewUtil.dpToPx(8)
|
||||
);
|
||||
constraints.setMargin(
|
||||
R.id.call_participant_switch_camera,
|
||||
ConstraintSet.BOTTOM,
|
||||
ViewUtil.dpToPx(8)
|
||||
);
|
||||
constraints.constrainWidth(R.id.call_participant_switch_camera, ViewUtil.dpToPx(48));
|
||||
constraints.constrainHeight(R.id.call_participant_switch_camera, ViewUtil.dpToPx(48));
|
||||
if (isMoreThanOneCameraAvailable) {
|
||||
constraints.setVisibility(R.id.call_participant_switch_camera, View.VISIBLE);
|
||||
constraints.setMargin(
|
||||
R.id.call_participant_switch_camera,
|
||||
ConstraintSet.END,
|
||||
ViewUtil.dpToPx(8)
|
||||
);
|
||||
constraints.setMargin(
|
||||
R.id.call_participant_switch_camera,
|
||||
ConstraintSet.BOTTOM,
|
||||
ViewUtil.dpToPx(8)
|
||||
);
|
||||
constraints.constrainWidth(R.id.call_participant_switch_camera, ViewUtil.dpToPx(48));
|
||||
constraints.constrainHeight(R.id.call_participant_switch_camera, ViewUtil.dpToPx(48));
|
||||
|
||||
ViewGroup.LayoutParams params = switchCameraIcon.getLayoutParams();
|
||||
params.width = params.height = ViewUtil.dpToPx(24);
|
||||
switchCameraIcon.setLayoutParams(params);
|
||||
ViewGroup.LayoutParams params = switchCameraIcon.getLayoutParams();
|
||||
params.width = params.height = ViewUtil.dpToPx(24);
|
||||
switchCameraIcon.setLayoutParams(params);
|
||||
|
||||
switchCameraIconFrame.setClickable(true);
|
||||
switchCameraIconFrame.setEnabled(true);
|
||||
switchCameraIconFrame.setClickable(true);
|
||||
switchCameraIconFrame.setEnabled(true);
|
||||
} else {
|
||||
constraints.setVisibility(R.id.call_participant_switch_camera, View.GONE);
|
||||
}
|
||||
}
|
||||
case MINI_SELF_PIP -> {
|
||||
constraints.connect(
|
||||
|
|
|
@ -16,7 +16,6 @@ import android.widget.FrameLayout;
|
|||
import androidx.annotation.NonNull;
|
||||
import androidx.core.view.GestureDetectorCompat;
|
||||
|
||||
import org.signal.core.util.DimensionUnit;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.views.TouchInterceptingFrameLayout;
|
||||
|
@ -25,10 +24,9 @@ import java.util.Arrays;
|
|||
|
||||
public class PictureInPictureGestureHelper extends GestureDetector.SimpleOnGestureListener {
|
||||
|
||||
private static final float DECELERATION_RATE = 0.99f;
|
||||
private static final Interpolator FLING_INTERPOLATOR = new ViscousFluidInterpolator();
|
||||
private static final Interpolator ADJUST_INTERPOLATOR = new AccelerateDecelerateInterpolator();
|
||||
private static final int HORIZONTAL_PARTICIPANTS_CONTAINER_HEIGHT = (int) DimensionUnit.DP.toPixels(36);
|
||||
private static final float DECELERATION_RATE = 0.99f;
|
||||
private static final Interpolator FLING_INTERPOLATOR = new ViscousFluidInterpolator();
|
||||
private static final Interpolator ADJUST_INTERPOLATOR = new AccelerateDecelerateInterpolator();
|
||||
|
||||
private final ViewGroup parent;
|
||||
private final View child;
|
||||
|
@ -36,7 +34,7 @@ public class PictureInPictureGestureHelper extends GestureDetector.SimpleOnGestu
|
|||
|
||||
private int pipWidth;
|
||||
private int pipHeight;
|
||||
private int activePointerId = MotionEvent.INVALID_POINTER_ID;
|
||||
private int activePointerId = MotionEvent.INVALID_POINTER_ID;
|
||||
private float lastTouchX;
|
||||
private float lastTouchY;
|
||||
private int extraPaddingTop;
|
||||
|
@ -47,10 +45,9 @@ public class PictureInPictureGestureHelper extends GestureDetector.SimpleOnGestu
|
|||
private int maximumFlingVelocity;
|
||||
private boolean isLockedToBottomEnd;
|
||||
private Interpolator interpolator;
|
||||
private Corner currentCornerPosition = Corner.BOTTOM_RIGHT;
|
||||
private int previousTopBoundary = -1;
|
||||
private int previousBottomBoundary = -1;
|
||||
private boolean displayBelowVerticalBoundary = false;
|
||||
private Corner currentCornerPosition = Corner.BOTTOM_RIGHT;
|
||||
private int previousTopBoundary = -1;
|
||||
private int previousBottomBoundary = -1;
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
public static PictureInPictureGestureHelper applyTo(@NonNull View child) {
|
||||
|
@ -135,27 +132,9 @@ public class PictureInPictureGestureHelper extends GestureDetector.SimpleOnGestu
|
|||
|
||||
extraPaddingBottom = parent.getMeasuredHeight() + parent.getTop() - bottomBoundary;
|
||||
|
||||
if (displayBelowVerticalBoundary) {
|
||||
extraPaddingBottom -= (int) DimensionUnit.DP.toPixels(HORIZONTAL_PARTICIPANTS_CONTAINER_HEIGHT);
|
||||
}
|
||||
|
||||
ViewUtil.setBottomMargin(child, extraPaddingBottom + framePadding);
|
||||
}
|
||||
|
||||
public void setDisplayBelowVerticalBoundary(boolean displayBelowVerticalBoundary) {
|
||||
if (this.displayBelowVerticalBoundary == displayBelowVerticalBoundary) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.displayBelowVerticalBoundary = displayBelowVerticalBoundary;
|
||||
|
||||
int bottomBoundary = previousBottomBoundary;
|
||||
|
||||
previousBottomBoundary = -1;
|
||||
|
||||
setBottomVerticalBoundary(bottomBoundary);
|
||||
}
|
||||
|
||||
private boolean onGestureFinished(MotionEvent e) {
|
||||
final int pointerIndex = e.findPointerIndex(activePointerId);
|
||||
|
||||
|
@ -322,7 +301,7 @@ public class PictureInPictureGestureHelper extends GestureDetector.SimpleOnGestu
|
|||
* User drag is implemented by translating the view from the current gravity anchor (corner). When the user drags
|
||||
* to a new corner, we need to adjust the translations for the new corner so the animation of translation X/Y to 0
|
||||
* works correctly.
|
||||
* <p>
|
||||
*
|
||||
* For example, if in bottom right and need to move to top right, we need to calculate a new translation Y since instead
|
||||
* of being translated up from bottom it's translated down from the top.
|
||||
*/
|
||||
|
|
|
@ -96,7 +96,6 @@ public class WebRtcCallView extends InsetAwareConstraintLayout {
|
|||
private RecipientId recipientId;
|
||||
private ImageView answer;
|
||||
private TextView answerWithoutVideoLabel;
|
||||
private ImageView cameraDirectionToggle;
|
||||
private AccessibleToggleButton ringToggle;
|
||||
private PictureInPictureGestureHelper pictureInPictureGestureHelper;
|
||||
private ImageView overflow;
|
||||
|
@ -178,7 +177,6 @@ public class WebRtcCallView extends InsetAwareConstraintLayout {
|
|||
incomingRingStatus = findViewById(R.id.call_screen_incoming_ring_status);
|
||||
answer = findViewById(R.id.call_screen_answer_call);
|
||||
answerWithoutVideoLabel = findViewById(R.id.call_screen_answer_without_video_label);
|
||||
cameraDirectionToggle = findViewById(R.id.call_screen_camera_direction_toggle);
|
||||
ringToggle = findViewById(R.id.call_screen_audio_ring_toggle);
|
||||
overflow = findViewById(R.id.call_screen_overflow_button);
|
||||
hangup = findViewById(R.id.call_screen_end_call);
|
||||
|
@ -273,7 +271,6 @@ public class WebRtcCallView extends InsetAwareConstraintLayout {
|
|||
runIfNonNull(controlsListener, listener -> listener.onRingGroupChanged(isOn, ringToggle.isActivated()));
|
||||
});
|
||||
|
||||
cameraDirectionToggle.setOnClickListener(v -> runIfNonNull(controlsListener, ControlsListener::onCameraDirectionChanged));
|
||||
smallLocalRender.findViewById(R.id.call_participant_switch_camera).setOnClickListener(v -> runIfNonNull(controlsListener, ControlsListener::onCameraDirectionChanged));
|
||||
|
||||
overflow.setOnClickListener(v -> {
|
||||
|
@ -355,7 +352,6 @@ public class WebRtcCallView extends InsetAwareConstraintLayout {
|
|||
rotatableControls.add(audioToggle);
|
||||
rotatableControls.add(micToggle);
|
||||
rotatableControls.add(videoToggle);
|
||||
rotatableControls.add(cameraDirectionToggle);
|
||||
rotatableControls.add(decline);
|
||||
rotatableControls.add(smallLocalAudioIndicator);
|
||||
rotatableControls.add(ringToggle);
|
||||
|
@ -510,19 +506,17 @@ public class WebRtcCallView extends InsetAwareConstraintLayout {
|
|||
|
||||
if (state == WebRtcLocalRenderState.EXPANDED) {
|
||||
pictureInPictureExpansionHelper.beginExpandTransition();
|
||||
smallLocalRender.setSelfPipMode(CallParticipantView.SelfPipMode.EXPANDED_SELF_PIP);
|
||||
smallLocalRender.setSelfPipMode(CallParticipantView.SelfPipMode.EXPANDED_SELF_PIP, localCallParticipant.isMoreThanOneCameraAvailable());
|
||||
return;
|
||||
} else if ((state.isAnySmall() || state == WebRtcLocalRenderState.GONE) && pictureInPictureExpansionHelper.isExpandedOrExpanding()) {
|
||||
pictureInPictureExpansionHelper.beginShrinkTransition();
|
||||
smallLocalRender.setSelfPipMode(pictureInPictureExpansionHelper.isMiniSize() ? CallParticipantView.SelfPipMode.MINI_SELF_PIP : CallParticipantView.SelfPipMode.NORMAL_SELF_PIP);
|
||||
smallLocalRender.setSelfPipMode(pictureInPictureExpansionHelper.isMiniSize() ? CallParticipantView.SelfPipMode.MINI_SELF_PIP : CallParticipantView.SelfPipMode.NORMAL_SELF_PIP, localCallParticipant.isMoreThanOneCameraAvailable());
|
||||
|
||||
if (state != WebRtcLocalRenderState.GONE) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
pictureInPictureGestureHelper.setDisplayBelowVerticalBoundary(false);
|
||||
|
||||
switch (state) {
|
||||
case GONE:
|
||||
largeLocalRender.attachBroadcastVideoSink(null);
|
||||
|
@ -532,18 +526,17 @@ public class WebRtcCallView extends InsetAwareConstraintLayout {
|
|||
break;
|
||||
case SMALL_RECTANGLE:
|
||||
smallLocalRenderFrame.setVisibility(View.VISIBLE);
|
||||
animatePipToLargeRectangle(displaySmallSelfPipInLandscape);
|
||||
animatePipToLargeRectangle(displaySmallSelfPipInLandscape, localCallParticipant.isMoreThanOneCameraAvailable());
|
||||
|
||||
largeLocalRender.attachBroadcastVideoSink(null);
|
||||
largeLocalRenderFrame.setVisibility(View.GONE);
|
||||
break;
|
||||
case SMALLER_RECTANGLE:
|
||||
smallLocalRenderFrame.setVisibility(View.VISIBLE);
|
||||
animatePipToSmallRectangle();
|
||||
animatePipToSmallRectangle(localCallParticipant.isMoreThanOneCameraAvailable());
|
||||
|
||||
largeLocalRender.attachBroadcastVideoSink(null);
|
||||
largeLocalRenderFrame.setVisibility(View.GONE);
|
||||
pictureInPictureGestureHelper.setDisplayBelowVerticalBoundary(true);
|
||||
break;
|
||||
case LARGE:
|
||||
largeLocalRender.attachBroadcastVideoSink(localCallParticipant.getVideoSink());
|
||||
|
@ -792,7 +785,7 @@ public class WebRtcCallView extends InsetAwareConstraintLayout {
|
|||
}
|
||||
}
|
||||
|
||||
private void animatePipToLargeRectangle(boolean isLandscape) {
|
||||
private void animatePipToLargeRectangle(boolean isLandscape, boolean moreThanOneCameraAvailable) {
|
||||
final Point dimens;
|
||||
if (isLandscape) {
|
||||
dimens = new Point(ViewUtil.dpToPx(PictureInPictureExpansionHelper.NORMAL_PIP_HEIGHT_DP),
|
||||
|
@ -809,10 +802,10 @@ public class WebRtcCallView extends InsetAwareConstraintLayout {
|
|||
}
|
||||
});
|
||||
|
||||
smallLocalRender.setSelfPipMode(CallParticipantView.SelfPipMode.NORMAL_SELF_PIP);
|
||||
smallLocalRender.setSelfPipMode(CallParticipantView.SelfPipMode.NORMAL_SELF_PIP, moreThanOneCameraAvailable);
|
||||
}
|
||||
|
||||
private void animatePipToSmallRectangle() {
|
||||
private void animatePipToSmallRectangle(boolean moreThanOneCameraAvailable) {
|
||||
pictureInPictureExpansionHelper.startDefaultSizeTransition(new Point(ViewUtil.dpToPx(PictureInPictureExpansionHelper.MINI_PIP_WIDTH_DP),
|
||||
ViewUtil.dpToPx(PictureInPictureExpansionHelper.MINI_PIP_HEIGHT_DP)),
|
||||
new PictureInPictureExpansionHelper.Callback() {
|
||||
|
@ -822,7 +815,7 @@ public class WebRtcCallView extends InsetAwareConstraintLayout {
|
|||
}
|
||||
});
|
||||
|
||||
smallLocalRender.setSelfPipMode(CallParticipantView.SelfPipMode.MINI_SELF_PIP);
|
||||
smallLocalRender.setSelfPipMode(CallParticipantView.SelfPipMode.MINI_SELF_PIP, moreThanOneCameraAvailable);
|
||||
}
|
||||
|
||||
private void toggleControls() {
|
||||
|
@ -924,7 +917,6 @@ public class WebRtcCallView extends InsetAwareConstraintLayout {
|
|||
}
|
||||
|
||||
private void updateButtonStateForLargeButtons() {
|
||||
cameraDirectionToggle.setImageResource(R.drawable.webrtc_call_screen_camera_toggle);
|
||||
hangup.setImageResource(R.drawable.webrtc_call_screen_hangup);
|
||||
overflow.setImageResource(R.drawable.webrtc_call_screen_overflow_menu);
|
||||
micToggle.setBackgroundResource(R.drawable.webrtc_call_screen_mic_toggle);
|
||||
|
@ -935,7 +927,6 @@ public class WebRtcCallView extends InsetAwareConstraintLayout {
|
|||
}
|
||||
|
||||
private void updateButtonStateForSmallButtons() {
|
||||
cameraDirectionToggle.setImageResource(R.drawable.webrtc_call_screen_camera_toggle_small);
|
||||
hangup.setImageResource(R.drawable.webrtc_call_screen_hangup_small);
|
||||
overflow.setImageResource(R.drawable.webrtc_call_screen_overflow_menu_small);
|
||||
micToggle.setBackgroundResource(R.drawable.webrtc_call_screen_mic_toggle_small);
|
||||
|
|
|
@ -97,7 +97,7 @@ class ControlsAndInfoController(
|
|||
private val handler: Handler?
|
||||
get() = webRtcCallView.handler
|
||||
|
||||
private var previousCallControlHeight = 0
|
||||
private var previousCallControlHeightData = HeightData()
|
||||
private var controlPeakHeight = 0
|
||||
private var controlState: WebRtcControls = WebRtcControls.NONE
|
||||
|
||||
|
@ -152,8 +152,9 @@ class ControlsAndInfoController(
|
|||
}
|
||||
|
||||
callControls.viewTreeObserver.addOnGlobalLayoutListener {
|
||||
if (callControls.height > 0 && callControls.height != previousCallControlHeight) {
|
||||
previousCallControlHeight = callControls.height
|
||||
if (callControls.height > 0 && previousCallControlHeightData.hasChanged(callControls.height, coordinator.height)) {
|
||||
previousCallControlHeightData = HeightData(callControls.height, coordinator.height)
|
||||
|
||||
controlPeakHeight = callControls.height + callControls.y.toInt()
|
||||
behavior.peekHeight = controlPeakHeight
|
||||
frame.minimumHeight = coordinator.height / 2
|
||||
|
@ -208,7 +209,7 @@ class ControlsAndInfoController(
|
|||
}
|
||||
}
|
||||
|
||||
fun onControlTopChanged() {
|
||||
private fun onControlTopChanged() {
|
||||
val guidelineTop = max(frame.top, coordinator.height - behavior.peekHeight)
|
||||
aboveControlsGuideline.setGuidelineBegin(guidelineTop)
|
||||
webRtcCallView.onControlTopChanged()
|
||||
|
@ -320,7 +321,6 @@ class ControlsAndInfoController(
|
|||
val margin = if (controlState.displaySmallCallButtons()) 4.dp else 8.dp
|
||||
|
||||
setControlConstraints(R.id.call_screen_speaker_toggle, controlState.displayAudioToggle(), margin)
|
||||
setControlConstraints(R.id.call_screen_camera_direction_toggle, controlState.displayCameraToggle(), margin)
|
||||
setControlConstraints(R.id.call_screen_video_toggle, controlState.displayVideoToggle(), margin)
|
||||
setControlConstraints(R.id.call_screen_audio_mic_toggle, controlState.displayMuteAudio(), margin)
|
||||
setControlConstraints(R.id.call_screen_audio_ring_toggle, controlState.displayRingToggle(), margin)
|
||||
|
@ -456,6 +456,15 @@ class ControlsAndInfoController(
|
|||
displayEndCall() != previousState.displayEndCall()
|
||||
}
|
||||
|
||||
private data class HeightData(
|
||||
val controlHeight: Int = 0,
|
||||
val coordinatorHeight: Int = 0
|
||||
) {
|
||||
fun hasChanged(controlHeight: Int, coordinatorHeight: Int): Boolean {
|
||||
return controlHeight != this.controlHeight || coordinatorHeight != this.coordinatorHeight
|
||||
}
|
||||
}
|
||||
|
||||
interface BottomSheetVisibilityListener {
|
||||
fun onShown()
|
||||
fun onHidden()
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
package org.thoughtcrime.securesms.compose
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.ViewCompositionStrategy
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import org.signal.core.ui.theme.SignalTheme
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme
|
||||
|
||||
/**
|
||||
* Generic ComposeFragment which can be subclassed to build UI with compose.
|
||||
*/
|
||||
abstract class ComposeFullScreenDialogFragment : DialogFragment() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setStyle(STYLE_NO_FRAME, R.style.Signal_DayNight_Dialog_FullScreen)
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
return ComposeView(requireContext()).apply {
|
||||
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
|
||||
setContent {
|
||||
SignalTheme(
|
||||
isDarkMode = DynamicTheme.isDarkTheme(LocalContext.current)
|
||||
) {
|
||||
DialogContent()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
abstract fun DialogContent()
|
||||
}
|
|
@ -1071,7 +1071,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
|||
boolean messageRequestAccepted,
|
||||
boolean allowedToPlayInline)
|
||||
{
|
||||
boolean showControls = messageRecord.isMediaPending() || (!messageRecord.isFailed() && !MessageRecordUtil.isScheduled(messageRecord));
|
||||
boolean showControls = !MessageRecordUtil.isScheduled(messageRecord) && (messageRecord.isMediaPending() || !messageRecord.isFailed());
|
||||
|
||||
ViewUtil.setTopMargin(bodyText, readDimen(R.dimen.message_bubble_top_padding));
|
||||
|
||||
|
|
|
@ -3134,6 +3134,7 @@ class ConversationFragment :
|
|||
searchNav.visible = true
|
||||
searchNav.setData(0, 0)
|
||||
inputPanel.setHideForSearch(true)
|
||||
binding.conversationDisabledInput.visible = false
|
||||
|
||||
(0 until menu.size()).forEach {
|
||||
if (menu.getItem(it) != searchMenuItem) {
|
||||
|
@ -3150,6 +3151,7 @@ class ConversationFragment :
|
|||
searchViewModel.onSearchClosed()
|
||||
searchNav.visible = false
|
||||
inputPanel.setHideForSearch(false)
|
||||
binding.conversationDisabledInput.visible = true
|
||||
viewModel.setSearchQuery(null)
|
||||
invalidateOptionsMenu()
|
||||
return true
|
||||
|
|
|
@ -41,6 +41,8 @@ import org.thoughtcrime.securesms.database.model.MessageRecord
|
|||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientForeverObserver
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.util.InterceptableLongClickCopyLinkSpan
|
||||
import org.thoughtcrime.securesms.util.LongClickMovementMethod
|
||||
|
@ -66,7 +68,7 @@ open class V2ConversationItemTextOnlyViewHolder<Model : MappingModel<Model>>(
|
|||
private val binding: V2ConversationItemTextOnlyBindingBridge,
|
||||
private val conversationContext: V2ConversationContext,
|
||||
footerDelegate: V2FooterPositionDelegate = V2FooterPositionDelegate(binding)
|
||||
) : V2ConversationItemViewHolder<Model>(binding.root, conversationContext), Multiselectable, InteractiveConversationElement {
|
||||
) : V2ConversationItemViewHolder<Model>(binding.root, conversationContext), Multiselectable, InteractiveConversationElement, RecipientForeverObserver {
|
||||
|
||||
companion object {
|
||||
private val STYLE_FACTORY = SearchUtil.StyleFactory { arrayOf<CharacterStyle>(BackgroundColorSpan(Color.YELLOW), ForegroundColorSpan(Color.BLACK)) }
|
||||
|
@ -189,7 +191,15 @@ open class V2ConversationItemTextOnlyViewHolder<Model : MappingModel<Model>>(
|
|||
}
|
||||
|
||||
check(model is ConversationMessageElement)
|
||||
|
||||
if (this::conversationMessage.isInitialized) {
|
||||
conversationMessage.messageRecord.fromRecipient.live().removeForeverObserver(this)
|
||||
}
|
||||
|
||||
conversationMessage = model.conversationMessage
|
||||
if (conversationMessage.threadRecipient.isGroup) {
|
||||
conversationMessage.messageRecord.fromRecipient.live().observeForever(this)
|
||||
}
|
||||
|
||||
shape = shapeDelegate.setMessageShape(
|
||||
currentMessage = conversationMessage.messageRecord,
|
||||
|
@ -765,4 +775,8 @@ open class V2ConversationItemTextOnlyViewHolder<Model : MappingModel<Model>>(
|
|||
return true
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRecipientChanged(recipient: Recipient) {
|
||||
presentSender()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -101,7 +101,6 @@ import org.thoughtcrime.securesms.components.reminder.CdsTemporaryErrorReminder;
|
|||
import org.thoughtcrime.securesms.components.reminder.DozeReminder;
|
||||
import org.thoughtcrime.securesms.components.reminder.ExpiredBuildReminder;
|
||||
import org.thoughtcrime.securesms.components.reminder.OutdatedBuildReminder;
|
||||
import org.thoughtcrime.securesms.components.reminder.PushRegistrationReminder;
|
||||
import org.thoughtcrime.securesms.components.reminder.Reminder;
|
||||
import org.thoughtcrime.securesms.components.reminder.ReminderView;
|
||||
import org.thoughtcrime.securesms.components.reminder.ServiceOutageReminder;
|
||||
|
@ -1000,8 +999,6 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
|||
return Optional.of(new ServiceOutageReminder());
|
||||
} else if (OutdatedBuildReminder.isEligible()) {
|
||||
return Optional.of(new OutdatedBuildReminder(context));
|
||||
} else if (PushRegistrationReminder.isEligible()) {
|
||||
return Optional.of((new PushRegistrationReminder(context)));
|
||||
} else if (DozeReminder.isEligible(context)) {
|
||||
return Optional.of(new DozeReminder(context));
|
||||
} else if (CdsTemporaryErrorReminder.isEligible()) {
|
||||
|
|
|
@ -110,6 +110,7 @@ class ConversationListViewModel(
|
|||
|
||||
hasNoConversations = store
|
||||
.stateFlowable
|
||||
.subscribeOn(Schedulers.io())
|
||||
.map { it.filterRequest to it.conversations }
|
||||
.distinctUntilChanged()
|
||||
.map { (filterRequest, conversations) ->
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -12,6 +12,7 @@ import org.signal.core.util.delete
|
|||
import org.signal.core.util.deleteAll
|
||||
import org.signal.core.util.flatten
|
||||
import org.signal.core.util.insertInto
|
||||
import org.signal.core.util.isAbsent
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.readToList
|
||||
import org.signal.core.util.readToMap
|
||||
|
@ -423,6 +424,7 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
|
|||
Log.w(TAG, "[acceptOutgoingGroupCall] This shouldn't have been an outgoing ring because the call already existed!")
|
||||
Event.ACCEPTED
|
||||
}
|
||||
|
||||
else -> {
|
||||
Log.d(TAG, "[acceptOutgoingGroupCall] Call in state ${call.event} cannot be transitioned by ACCEPTED")
|
||||
return
|
||||
|
@ -963,12 +965,12 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
|
|||
/**
|
||||
* Gets the most recent timestamp from the [TIMESTAMP] column
|
||||
*/
|
||||
fun getLatestTimestamp(): Long {
|
||||
fun getLatestCall(): Call? {
|
||||
val statement = """
|
||||
SELECT $TIMESTAMP FROM $TABLE_NAME ORDER BY $TIMESTAMP DESC LIMIT 1
|
||||
SELECT * FROM $TABLE_NAME ORDER BY $TIMESTAMP DESC LIMIT 1
|
||||
""".trimIndent()
|
||||
|
||||
return readableDatabase.query(statement).readToSingleLong(-1)
|
||||
return readableDatabase.query(statement).readToSingleObject { Call.deserialize(it) }
|
||||
}
|
||||
|
||||
fun deleteNonAdHocCallEventsOnOrBefore(timestamp: Long) {
|
||||
|
@ -1273,6 +1275,16 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
|
|||
val actualChildren = inPeriod.takeWhile { children.contains(it) }
|
||||
val peer = Recipient.resolved(call.peer)
|
||||
|
||||
val canUserBeginCall = if (peer.isGroup) {
|
||||
val record = SignalDatabase.groups.getGroup(peer.id)
|
||||
|
||||
!record.isAbsent() &&
|
||||
record.get().isActive &&
|
||||
(!record.get().isAnnouncementGroup || record.get().memberLevel(Recipient.self()) == GroupTable.MemberLevel.ADMINISTRATOR)
|
||||
} else {
|
||||
true
|
||||
}
|
||||
|
||||
CallLogRow.Call(
|
||||
record = call,
|
||||
date = call.timestamp,
|
||||
|
@ -1280,7 +1292,8 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
|
|||
groupCallState = CallLogRow.GroupCallState.fromDetails(groupCallDetails),
|
||||
children = actualChildren.toSet(),
|
||||
searchQuery = searchTerm,
|
||||
callLinkPeekInfo = ApplicationDependencies.getSignalCallManager().peekInfoSnapshot[peer.id]
|
||||
callLinkPeekInfo = ApplicationDependencies.getSignalCallManager().peekInfoSnapshot[peer.id],
|
||||
canUserBeginCall = canUserBeginCall
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -49,7 +49,7 @@ class MediaTable internal constructor(context: Context?, databaseHelper: SignalD
|
|||
${AttachmentTable.TABLE_NAME}.${AttachmentTable.UPLOAD_TIMESTAMP},
|
||||
${AttachmentTable.TABLE_NAME}.${AttachmentTable.REMOTE_INCREMENTAL_DIGEST},
|
||||
${AttachmentTable.TABLE_NAME}.${AttachmentTable.REMOTE_INCREMENTAL_DIGEST_CHUNK_SIZE},
|
||||
${AttachmentTable.TABLE_NAME}.${AttachmentTable.DATA_HASH},
|
||||
${AttachmentTable.TABLE_NAME}.${AttachmentTable.DATA_HASH_END},
|
||||
${MessageTable.TABLE_NAME}.${MessageTable.TYPE},
|
||||
${MessageTable.TABLE_NAME}.${MessageTable.DATE_SENT},
|
||||
${MessageTable.TABLE_NAME}.${MessageTable.DATE_RECEIVED},
|
||||
|
@ -71,13 +71,7 @@ class MediaTable internal constructor(context: Context?, databaseHelper: SignalD
|
|||
${MessageTable.VIEW_ONCE} = 0 AND
|
||||
${MessageTable.STORY_TYPE} = 0 AND
|
||||
${MessageTable.LATEST_REVISION_ID} IS NULL AND
|
||||
(
|
||||
${AttachmentTable.QUOTE} = 0 OR
|
||||
(
|
||||
${AttachmentTable.QUOTE} = 1 AND
|
||||
${AttachmentTable.DATA_HASH} IS NULL
|
||||
)
|
||||
) AND
|
||||
${AttachmentTable.QUOTE} = 0 AND
|
||||
${AttachmentTable.STICKER_PACK_ID} IS NULL AND
|
||||
${MessageTable.TABLE_NAME}.${MessageTable.FROM_RECIPIENT_ID} > 0 AND
|
||||
$THREAD_RECIPIENT_ID > 0
|
||||
|
|
|
@ -136,7 +136,6 @@ import org.thoughtcrime.securesms.util.MessageConstraintsUtil
|
|||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import org.thoughtcrime.securesms.util.Util
|
||||
import org.thoughtcrime.securesms.util.isStory
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.ReadMessage
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import org.whispersystems.signalservice.internal.push.SyncMessage
|
||||
import java.io.Closeable
|
||||
|
@ -4393,17 +4392,17 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
|||
/**
|
||||
* @return Unhandled ids
|
||||
*/
|
||||
fun setTimestampReadFromSyncMessage(readMessages: List<ReadMessage>, proposedExpireStarted: Long, threadToLatestRead: MutableMap<Long, Long>): Collection<SyncMessageId> {
|
||||
fun setTimestampReadFromSyncMessage(readMessages: List<SyncMessage.Read>, proposedExpireStarted: Long, threadToLatestRead: MutableMap<Long, Long>): Collection<SyncMessageId> {
|
||||
val expiringMessages: MutableList<Pair<Long, Long>> = mutableListOf()
|
||||
val updatedThreads: MutableSet<Long> = mutableSetOf()
|
||||
val unhandled: MutableCollection<SyncMessageId> = mutableListOf()
|
||||
|
||||
writableDatabase.withinTransaction {
|
||||
for (readMessage in readMessages) {
|
||||
val authorId: RecipientId = recipients.getOrInsertFromServiceId(readMessage.sender)
|
||||
val authorId: RecipientId = recipients.getOrInsertFromServiceId(ServiceId.parseOrThrow(readMessage.senderAci!!))
|
||||
|
||||
val result: TimestampReadResult = setTimestampReadFromSyncMessageInternal(
|
||||
messageId = SyncMessageId(authorId, readMessage.timestamp),
|
||||
messageId = SyncMessageId(authorId, readMessage.timestamp!!),
|
||||
proposedExpireStarted = proposedExpireStarted,
|
||||
threadToLatestRead = threadToLatestRead
|
||||
)
|
||||
|
@ -4412,7 +4411,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
|||
updatedThreads += result.threads
|
||||
|
||||
if (result.threads.isEmpty()) {
|
||||
unhandled += SyncMessageId(authorId, readMessage.timestamp)
|
||||
unhandled += SyncMessageId(authorId, readMessage.timestamp!!)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4433,12 +4432,6 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
|||
return unhandled
|
||||
}
|
||||
|
||||
fun setTimestampReadFromSyncMessageProto(readMessages: List<SyncMessage.Read>, proposedExpireStarted: Long, threadToLatestRead: MutableMap<Long, Long>): Collection<SyncMessageId> {
|
||||
val reads: List<ReadMessage> = readMessages.map { r -> ReadMessage(ServiceId.parseOrThrow(r.senderAci!!), r.timestamp!!) }
|
||||
|
||||
return setTimestampReadFromSyncMessage(reads, proposedExpireStarted, threadToLatestRead)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a synchronized read message.
|
||||
* @param messageId An id representing the author-timestamp pair of the message that was read on a linked device. Note that the author could be self when
|
||||
|
|
|
@ -184,6 +184,10 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
|
|||
const val PHONE_NUMBER_SHARING = "phone_number_sharing"
|
||||
const val PHONE_NUMBER_DISCOVERABLE = "phone_number_discoverable"
|
||||
const val PNI_SIGNATURE_VERIFIED = "pni_signature_verified"
|
||||
const val NICKNAME_GIVEN_NAME = "nickname_given_name"
|
||||
const val NICKNAME_FAMILY_NAME = "nickname_family_name"
|
||||
const val NICKNAME_JOINED_NAME = "nickname_joined_name"
|
||||
const val NOTE = "note"
|
||||
|
||||
const val SEARCH_PROFILE_NAME = "search_signal_profile"
|
||||
const val SORT_NAME = "sort_name"
|
||||
|
@ -252,7 +256,11 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
|
|||
$REPORTING_TOKEN BLOB DEFAULT NULL,
|
||||
$PHONE_NUMBER_SHARING INTEGER DEFAULT ${PhoneNumberSharingState.UNKNOWN.id},
|
||||
$PHONE_NUMBER_DISCOVERABLE INTEGER DEFAULT ${PhoneNumberDiscoverableState.UNKNOWN.id},
|
||||
$PNI_SIGNATURE_VERIFIED INTEGER DEFAULT 0
|
||||
$PNI_SIGNATURE_VERIFIED INTEGER DEFAULT 0,
|
||||
$NICKNAME_GIVEN_NAME TEXT DEFAULT NULL,
|
||||
$NICKNAME_FAMILY_NAME TEXT DEFAULT NULL,
|
||||
$NICKNAME_JOINED_NAME TEXT DEFAULT NULL,
|
||||
$NOTE TEXT DEFAULT NULL
|
||||
)
|
||||
"""
|
||||
|
||||
|
@ -312,7 +320,10 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
|
|||
BADGES,
|
||||
NEEDS_PNI_SIGNATURE,
|
||||
REPORTING_TOKEN,
|
||||
PHONE_NUMBER_SHARING
|
||||
PHONE_NUMBER_SHARING,
|
||||
NICKNAME_GIVEN_NAME,
|
||||
NICKNAME_FAMILY_NAME,
|
||||
NOTE
|
||||
)
|
||||
|
||||
private val ID_PROJECTION = arrayOf(ID)
|
||||
|
@ -333,6 +344,8 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
|
|||
"""
|
||||
LOWER(
|
||||
COALESCE(
|
||||
NULLIF($NICKNAME_JOINED_NAME, ''),
|
||||
NULLIF($NICKNAME_GIVEN_NAME, ''),
|
||||
NULLIF($SYSTEM_JOINED_NAME, ''),
|
||||
NULLIF($SYSTEM_GIVEN_NAME, ''),
|
||||
NULLIF($PROFILE_JOINED_NAME, ''),
|
||||
|
@ -372,6 +385,8 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
|
|||
"""
|
||||
REPLACE(
|
||||
COALESCE(
|
||||
NULLIF($NICKNAME_JOINED_NAME, ''),
|
||||
NULLIF($NICKNAME_GIVEN_NAME, ''),
|
||||
NULLIF($SYSTEM_JOINED_NAME, ''),
|
||||
NULLIF($SYSTEM_GIVEN_NAME, ''),
|
||||
NULLIF($PROFILE_JOINED_NAME, ''),
|
||||
|
@ -1722,6 +1737,20 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
|
|||
}
|
||||
}
|
||||
|
||||
fun setNicknameAndNote(id: RecipientId, nickname: ProfileName, note: String) {
|
||||
val contentValues = contentValuesOf(
|
||||
NICKNAME_GIVEN_NAME to nickname.givenName.nullIfBlank(),
|
||||
NICKNAME_FAMILY_NAME to nickname.familyName.nullIfBlank(),
|
||||
NICKNAME_JOINED_NAME to nickname.toString().nullIfBlank(),
|
||||
NOTE to note.nullIfBlank()
|
||||
)
|
||||
if (update(id, contentValues)) {
|
||||
rotateStorageId(id)
|
||||
ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id)
|
||||
StorageSyncHelper.scheduleSyncForDataChange()
|
||||
}
|
||||
}
|
||||
|
||||
fun setProfileName(id: RecipientId, profileName: ProfileName) {
|
||||
val contentValues = ContentValues(1).apply {
|
||||
put(PROFILE_GIVEN_NAME, profileName.givenName.nullIfBlank())
|
||||
|
@ -3207,6 +3236,13 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
|
|||
val args = searchSelection.args
|
||||
val orderBy = "${if (contactSearchQuery.contactSearchSortOrder == ContactSearchSortOrder.RECENCY) "${ThreadTable.TABLE_NAME}.${ThreadTable.DATE} DESC, " else ""}$SORT_NAME, $SYSTEM_JOINED_NAME, $SEARCH_PROFILE_NAME, $E164"
|
||||
|
||||
//language=roomsql
|
||||
val join = if (contactSearchQuery.contactSearchSortOrder == ContactSearchSortOrder.RECENCY) {
|
||||
"LEFT OUTER JOIN ${ThreadTable.TABLE_NAME} ON ${ThreadTable.TABLE_NAME}.${ThreadTable.RECIPIENT_ID} = $TABLE_NAME.$ID"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
|
||||
return if (contactSearchQuery.contactSearchSortOrder == ContactSearchSortOrder.RECENCY) {
|
||||
val ambiguous = listOf(ID)
|
||||
val projection = SEARCH_PROJECTION.map {
|
||||
|
@ -3218,7 +3254,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
|
|||
"""
|
||||
SELECT ${projection.joinToString(",")}
|
||||
FROM $TABLE_NAME
|
||||
JOIN ${ThreadTable.TABLE_NAME} ON ${ThreadTable.TABLE_NAME}.${ThreadTable.RECIPIENT_ID} = $TABLE_NAME.$ID
|
||||
$join
|
||||
WHERE $selection
|
||||
ORDER BY $orderBy
|
||||
""".trimIndent(),
|
||||
|
@ -3935,6 +3971,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
|
|||
val profileName = ProfileName.fromParts(contact.profileGivenName.orElse(null), contact.profileFamilyName.orElse(null))
|
||||
val systemName = ProfileName.fromParts(contact.systemGivenName.orElse(null), contact.systemFamilyName.orElse(null))
|
||||
val username = contact.username.orElse(null)
|
||||
val nickname = ProfileName.fromParts(contact.nicknameGivenName.orNull(), contact.nicknameFamilyName.orNull())
|
||||
|
||||
put(ACI_COLUMN, contact.aci.orElse(null)?.toString())
|
||||
put(PNI_COLUMN, contact.pni.orElse(null)?.toString())
|
||||
|
@ -3954,6 +3991,10 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
|
|||
put(STORAGE_SERVICE_ID, Base64.encodeWithPadding(contact.id.raw))
|
||||
put(HIDDEN, contact.isHidden)
|
||||
put(PNI_SIGNATURE_VERIFIED, contact.isPniSignatureVerified.toInt())
|
||||
put(NICKNAME_GIVEN_NAME, nickname.givenName.nullIfBlank())
|
||||
put(NICKNAME_FAMILY_NAME, nickname.familyName.nullIfBlank())
|
||||
put(NICKNAME_JOINED_NAME, nickname.toString().nullIfBlank())
|
||||
put(NOTE, contact.note.orNull().nullIfBlank())
|
||||
|
||||
if (contact.hasUnknownFields()) {
|
||||
put(STORAGE_SERVICE_PROTO, Base64.encodeWithPadding(Objects.requireNonNull(contact.serializeUnknownFields())))
|
||||
|
|
|
@ -165,7 +165,9 @@ object RecipientTableCursorUtil {
|
|||
needsPniSignature = cursor.requireBoolean(RecipientTable.NEEDS_PNI_SIGNATURE),
|
||||
hiddenState = Recipient.HiddenState.deserialize(cursor.requireInt(RecipientTable.HIDDEN)),
|
||||
callLinkRoomId = cursor.requireString(RecipientTable.CALL_LINK_ROOM_ID)?.let { CallLinkRoomId.DatabaseSerializer.deserialize(it) },
|
||||
phoneNumberSharing = cursor.requireInt(RecipientTable.PHONE_NUMBER_SHARING).let { RecipientTable.PhoneNumberSharingState.fromId(it) }
|
||||
phoneNumberSharing = cursor.requireInt(RecipientTable.PHONE_NUMBER_SHARING).let { RecipientTable.PhoneNumberSharingState.fromId(it) },
|
||||
nickname = ProfileName.fromParts(cursor.requireString(RecipientTable.NICKNAME_GIVEN_NAME), cursor.requireString(RecipientTable.NICKNAME_FAMILY_NAME)),
|
||||
note = cursor.requireString(RecipientTable.NOTE)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -79,6 +79,8 @@ import org.thoughtcrime.securesms.database.helpers.migration.V218_RecipientPniSi
|
|||
import org.thoughtcrime.securesms.database.helpers.migration.V219_PniPreKeyStores
|
||||
import org.thoughtcrime.securesms.database.helpers.migration.V220_PreKeyConstraints
|
||||
import org.thoughtcrime.securesms.database.helpers.migration.V221_AddReadColumnToCallEventsTable
|
||||
import org.thoughtcrime.securesms.database.helpers.migration.V222_DataHashRefactor
|
||||
import org.thoughtcrime.securesms.database.helpers.migration.V223_AddNicknameAndNoteFieldsToRecipientTable
|
||||
|
||||
/**
|
||||
* Contains all of the database migrations for [SignalDatabase]. Broken into a separate file for cleanliness.
|
||||
|
@ -161,10 +163,12 @@ object SignalDatabaseMigrations {
|
|||
218 to V218_RecipientPniSignatureVerified,
|
||||
219 to V219_PniPreKeyStores,
|
||||
220 to V220_PreKeyConstraints,
|
||||
221 to V221_AddReadColumnToCallEventsTable
|
||||
221 to V221_AddReadColumnToCallEventsTable,
|
||||
222 to V222_DataHashRefactor,
|
||||
223 to V223_AddNicknameAndNoteFieldsToRecipientTable
|
||||
)
|
||||
|
||||
const val DATABASE_VERSION = 221
|
||||
const val DATABASE_VERSION = 223
|
||||
|
||||
@JvmStatic
|
||||
fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.database.helpers.migration
|
||||
|
||||
import android.app.Application
|
||||
import net.zetetic.database.sqlcipher.SQLiteDatabase
|
||||
|
||||
/**
|
||||
* Adds the new data hash columns and indexes.
|
||||
*/
|
||||
@Suppress("ClassName")
|
||||
object V222_DataHashRefactor : SignalDatabaseMigration {
|
||||
override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||
db.execSQL("DROP INDEX attachment_data_hash_index")
|
||||
db.execSQL("ALTER TABLE attachment DROP COLUMN data_hash")
|
||||
|
||||
db.execSQL("ALTER TABLE attachment ADD COLUMN data_hash_start TEXT DEFAULT NULL")
|
||||
db.execSQL("ALTER TABLE attachment ADD COLUMN data_hash_end TEXT DEFAULT NULL")
|
||||
db.execSQL("CREATE INDEX attachment_data_hash_start_index ON attachment (data_hash_start)")
|
||||
db.execSQL("CREATE INDEX attachment_data_hash_end_index ON attachment (data_hash_end)")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.database.helpers.migration
|
||||
|
||||
import android.app.Application
|
||||
import net.zetetic.database.sqlcipher.SQLiteDatabase
|
||||
|
||||
/**
|
||||
* Adds necessary fields to the recipeints table for the nickname & notes feature.
|
||||
*/
|
||||
@Suppress("ClassName")
|
||||
object V223_AddNicknameAndNoteFieldsToRecipientTable : SignalDatabaseMigration {
|
||||
override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||
db.execSQL("ALTER TABLE recipient ADD COLUMN nickname_given_name TEXT DEFAULT NULL")
|
||||
db.execSQL("ALTER TABLE recipient ADD COLUMN nickname_family_name TEXT DEFAULT NULL")
|
||||
db.execSQL("ALTER TABLE recipient ADD COLUMN nickname_joined_name TEXT DEFAULT NULL")
|
||||
db.execSQL("ALTER TABLE recipient ADD COLUMN note TEXT DEFAULT NULL")
|
||||
}
|
||||
}
|
|
@ -17,6 +17,7 @@ import org.signal.libsignal.protocol.ecc.ECPublicKey;
|
|||
import org.signal.libsignal.protocol.util.ByteUtil;
|
||||
import org.thoughtcrime.securesms.devicelist.Device;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.registration.secondary.DeviceNameCipher;
|
||||
import org.thoughtcrime.securesms.util.AsyncLoader;
|
||||
import org.signal.core.util.Base64;
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
||||
|
@ -76,50 +77,19 @@ public class DeviceListLoader extends AsyncLoader<List<Device>> {
|
|||
throw new IOException("Got a DeviceName that wasn't properly populated.");
|
||||
}
|
||||
|
||||
return new Device(deviceInfo.getId(), new String(decryptName(deviceName, SignalStore.account().getAciIdentityKey())), deviceInfo.getCreated(), deviceInfo.getLastSeen());
|
||||
byte[] plaintext = DeviceNameCipher.decryptDeviceName(deviceName, SignalStore.account().getAciIdentityKey());
|
||||
if (plaintext == null) {
|
||||
throw new IOException("Failed to decrypt device name.");
|
||||
}
|
||||
|
||||
return new Device(deviceInfo.getId(), new String(plaintext), deviceInfo.getCreated(), deviceInfo.getLastSeen());
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Failed while reading the protobuf.", e);
|
||||
} catch (GeneralSecurityException | InvalidKeyException e) {
|
||||
Log.w(TAG, "Failed during decryption.", e);
|
||||
}
|
||||
|
||||
return new Device(deviceInfo.getId(), deviceInfo.getName(), deviceInfo.getCreated(), deviceInfo.getLastSeen());
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public static byte[] decryptName(DeviceName deviceName, IdentityKeyPair identityKeyPair) throws InvalidKeyException, GeneralSecurityException {
|
||||
byte[] syntheticIv = Objects.requireNonNull(deviceName.syntheticIv).toByteArray();
|
||||
byte[] cipherText = Objects.requireNonNull(deviceName.ciphertext).toByteArray();
|
||||
ECPrivateKey identityKey = identityKeyPair.getPrivateKey();
|
||||
ECPublicKey ephemeralPublic = Curve.decodePoint(Objects.requireNonNull(deviceName.ephemeralPublic).toByteArray(), 0);
|
||||
byte[] masterSecret = Curve.calculateAgreement(ephemeralPublic, identityKey);
|
||||
|
||||
Mac mac = Mac.getInstance("HmacSHA256");
|
||||
mac.init(new SecretKeySpec(masterSecret, "HmacSHA256"));
|
||||
byte[] cipherKeyPart1 = mac.doFinal("cipher".getBytes());
|
||||
|
||||
mac.init(new SecretKeySpec(cipherKeyPart1, "HmacSHA256"));
|
||||
byte[] cipherKey = mac.doFinal(syntheticIv);
|
||||
|
||||
Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding");
|
||||
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(cipherKey, "AES"), new IvParameterSpec(new byte[16]));
|
||||
final byte[] plaintext = cipher.doFinal(cipherText);
|
||||
|
||||
mac.init(new SecretKeySpec(masterSecret, "HmacSHA256"));
|
||||
byte[] verificationPart1 = mac.doFinal("auth".getBytes());
|
||||
|
||||
mac.init(new SecretKeySpec(verificationPart1, "HmacSHA256"));
|
||||
byte[] verificationPart2 = mac.doFinal(plaintext);
|
||||
byte[] ourSyntheticIv = ByteUtil.trim(verificationPart2, 16);
|
||||
|
||||
if (!MessageDigest.isEqual(ourSyntheticIv, syntheticIv)) {
|
||||
throw new GeneralSecurityException("The computed syntheticIv didn't match the actual syntheticIv.");
|
||||
}
|
||||
|
||||
return plaintext;
|
||||
}
|
||||
|
||||
private static class DeviceComparator implements Comparator<Device> {
|
||||
|
||||
@Override
|
||||
|
|
|
@ -234,15 +234,24 @@ final class GroupsV2UpdateMessageProducer {
|
|||
}
|
||||
}
|
||||
private void describeGroupExpirationTimerUpdate(@NonNull GroupExpirationTimerUpdate update, @NonNull List<UpdateDescription> updates) {
|
||||
String time = ExpirationUtil.getExpirationDisplayValue(context, update.expiresInMs / 1000);
|
||||
final int duration = update.expiresInMs / 1000;
|
||||
String time = ExpirationUtil.getExpirationDisplayValue(context, duration);
|
||||
if (update.updaterAci == null) {
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_disappearing_message_time_set_to_s, time), R.drawable.ic_update_timer_16));
|
||||
} else {
|
||||
boolean editorIsYou = selfIds.matches(update.updaterAci);
|
||||
if (editorIsYou) {
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_set_disappearing_message_time_to_s, time), R.drawable.ic_update_timer_16));
|
||||
if (duration <= 0) {
|
||||
if (editorIsYou) {
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_disabled_disappearing_messages), R.drawable.ic_update_timer_16));
|
||||
} else {
|
||||
updates.add(updateDescription(R.string.MessageRecord_s_disabled_disappearing_messages, update.updaterAci, R.drawable.ic_update_timer_16));
|
||||
}
|
||||
} else {
|
||||
updates.add(updateDescription(R.string.MessageRecord_s_set_disappearing_message_time_to_s, update.updaterAci, time, R.drawable.ic_update_timer_16));
|
||||
if (editorIsYou) {
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_set_disappearing_message_time_to_s, time), R.drawable.ic_update_timer_16));
|
||||
} else {
|
||||
updates.add(updateDescription(R.string.MessageRecord_s_set_disappearing_message_time_to_s, update.updaterAci, time, R.drawable.ic_update_timer_16));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1140,11 +1149,20 @@ final class GroupsV2UpdateMessageProducer {
|
|||
boolean editorIsYou = selfIds.matches(change.editorServiceIdBytes);
|
||||
|
||||
if (change.newTimer != null) {
|
||||
String time = ExpirationUtil.getExpirationDisplayValue(context, change.newTimer.duration);
|
||||
if (editorIsYou) {
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_set_disappearing_message_time_to_s, time), R.drawable.ic_update_timer_16));
|
||||
final int duration = change.newTimer.duration;
|
||||
if (duration <= 0) {
|
||||
if (editorIsYou) {
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_disabled_disappearing_messages), R.drawable.ic_update_timer_16));
|
||||
} else {
|
||||
updates.add(updateDescription(R.string.MessageRecord_s_disabled_disappearing_messages, change.editorServiceIdBytes, R.drawable.ic_update_timer_16));
|
||||
}
|
||||
} else {
|
||||
updates.add(updateDescription(R.string.MessageRecord_s_set_disappearing_message_time_to_s, change.editorServiceIdBytes, time, R.drawable.ic_update_timer_16));
|
||||
String time = ExpirationUtil.getExpirationDisplayValue(context, duration);
|
||||
if (editorIsYou) {
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_set_disappearing_message_time_to_s, time), R.drawable.ic_update_timer_16));
|
||||
} else {
|
||||
updates.add(updateDescription(R.string.MessageRecord_s_set_disappearing_message_time_to_s, change.editorServiceIdBytes, time, R.drawable.ic_update_timer_16));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -78,7 +78,9 @@ data class RecipientRecord(
|
|||
val needsPniSignature: Boolean,
|
||||
val hiddenState: Recipient.HiddenState,
|
||||
val callLinkRoomId: CallLinkRoomId?,
|
||||
val phoneNumberSharing: PhoneNumberSharingState
|
||||
val phoneNumberSharing: PhoneNumberSharingState,
|
||||
val nickname: ProfileName,
|
||||
val note: String?
|
||||
) {
|
||||
|
||||
fun e164Only(): Boolean {
|
||||
|
|
|
@ -85,13 +85,13 @@ public class DeleteAccountFragment extends Fragment {
|
|||
}
|
||||
|
||||
private @NonNull CharSequence buildBulletsText(@NonNull Optional<String> formattedBalance) {
|
||||
SpannableStringBuilder builder = new SpannableStringBuilder().append(SpanUtil.bullet(getString(R.string.DeleteAccountFragment__delete_your_account_info_and_profile_photo)))
|
||||
SpannableStringBuilder builder = new SpannableStringBuilder().append(SpanUtil.bullet(getString(R.string.DeleteAccountFragment__delete_your_account_info_and_profile_photo),8))
|
||||
.append("\n")
|
||||
.append(SpanUtil.bullet(getString(R.string.DeleteAccountFragment__delete_all_your_messages)));
|
||||
.append(SpanUtil.bullet(getString(R.string.DeleteAccountFragment__delete_all_your_messages),8));
|
||||
|
||||
if (formattedBalance.isPresent()) {
|
||||
builder.append("\n");
|
||||
builder.append(SpanUtil.bullet(getString(R.string.DeleteAccountFragment__delete_s_in_your_payments_account, formattedBalance.get())));
|
||||
builder.append(SpanUtil.bullet(getString(R.string.DeleteAccountFragment__delete_s_in_your_payments_account, formattedBalance.get()),8));
|
||||
}
|
||||
|
||||
return builder;
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.devicetransfer.moreoptions
|
||||
|
||||
/**
|
||||
* Allows component opening sheet to specify mode
|
||||
*/
|
||||
enum class MoreTransferOrRestoreOptionsMode {
|
||||
/**
|
||||
* Only display the option to log in without transferring. Selection
|
||||
* will be disabled.
|
||||
*/
|
||||
SKIP_ONLY,
|
||||
|
||||
/**
|
||||
* Display transfer/restore local/skip as well as a next and cancel button
|
||||
*/
|
||||
SELECTION
|
||||
}
|
|
@ -0,0 +1,338 @@
|
|||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.devicetransfer.moreoptions
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.dimensionResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import org.signal.core.ui.BottomSheets
|
||||
import org.signal.core.ui.Buttons
|
||||
import org.signal.core.ui.Previews
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.compose.ComposeBottomSheetDialogFragment
|
||||
import org.thoughtcrime.securesms.devicetransfer.newdevice.BackupRestorationType
|
||||
|
||||
/**
|
||||
* Lists a set of options the user can choose from for restoring backup or skipping restoration
|
||||
*/
|
||||
class MoreTransferOrRestoreOptionsSheet : ComposeBottomSheetDialogFragment() {
|
||||
|
||||
private val args by navArgs<MoreTransferOrRestoreOptionsSheetArgs>()
|
||||
|
||||
@Composable
|
||||
override fun SheetContent() {
|
||||
var selectedOption by remember {
|
||||
mutableStateOf<BackupRestorationType?>(null)
|
||||
}
|
||||
|
||||
MoreOptionsSheetContent(
|
||||
mode = args.mode,
|
||||
selectedOption = selectedOption,
|
||||
onOptionSelected = { selectedOption = it },
|
||||
onCancelClick = { findNavController().popBackStack() },
|
||||
onNextClick = {
|
||||
this.onNextClicked(selectedOption ?: BackupRestorationType.NONE)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun onNextClicked(selectedOption: BackupRestorationType) {
|
||||
// TODO [message-requests] -- Launch next screen based off user choice
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun MoreOptionsSheetContentPreview() {
|
||||
Previews.BottomSheetPreview {
|
||||
MoreOptionsSheetContent(
|
||||
mode = MoreTransferOrRestoreOptionsMode.SKIP_ONLY,
|
||||
selectedOption = null,
|
||||
onOptionSelected = {},
|
||||
onCancelClick = {},
|
||||
onNextClick = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun MoreOptionsSheetSelectableContentPreview() {
|
||||
Previews.BottomSheetPreview {
|
||||
MoreOptionsSheetContent(
|
||||
mode = MoreTransferOrRestoreOptionsMode.SELECTION,
|
||||
selectedOption = null,
|
||||
onOptionSelected = {},
|
||||
onCancelClick = {},
|
||||
onNextClick = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MoreOptionsSheetContent(
|
||||
mode: MoreTransferOrRestoreOptionsMode,
|
||||
selectedOption: BackupRestorationType?,
|
||||
onOptionSelected: (BackupRestorationType) -> Unit,
|
||||
onCancelClick: () -> Unit,
|
||||
onNextClick: () -> Unit
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter))
|
||||
) {
|
||||
BottomSheets.Handle()
|
||||
|
||||
Spacer(modifier = Modifier.size(42.dp))
|
||||
|
||||
if (mode == MoreTransferOrRestoreOptionsMode.SELECTION) {
|
||||
TransferFromAndroidDeviceOption(
|
||||
selectedOption = selectedOption,
|
||||
onOptionSelected = onOptionSelected
|
||||
)
|
||||
Spacer(modifier = Modifier.size(16.dp))
|
||||
RestoreLocalBackupOption(
|
||||
selectedOption = selectedOption,
|
||||
onOptionSelected = onOptionSelected
|
||||
)
|
||||
Spacer(modifier = Modifier.size(16.dp))
|
||||
}
|
||||
|
||||
LogInWithoutTransferringOption(
|
||||
selectedOption = selectedOption,
|
||||
onOptionSelected = when (mode) {
|
||||
MoreTransferOrRestoreOptionsMode.SKIP_ONLY -> { _ -> onNextClick() }
|
||||
MoreTransferOrRestoreOptionsMode.SELECTION -> onOptionSelected
|
||||
}
|
||||
)
|
||||
|
||||
if (mode == MoreTransferOrRestoreOptionsMode.SELECTION) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 30.dp, bottom = 24.dp)
|
||||
) {
|
||||
TextButton(
|
||||
onClick = onCancelClick
|
||||
) {
|
||||
Text(text = stringResource(id = android.R.string.cancel))
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
Buttons.LargeTonal(
|
||||
enabled = selectedOption != null,
|
||||
onClick = onNextClick
|
||||
) {
|
||||
Text(text = stringResource(id = R.string.RegistrationActivity_next))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Spacer(modifier = Modifier.size(45.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun LogInWithoutTransferringOptionPreview() {
|
||||
Previews.BottomSheetPreview {
|
||||
LogInWithoutTransferringOption(
|
||||
selectedOption = null,
|
||||
onOptionSelected = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LogInWithoutTransferringOption(
|
||||
selectedOption: BackupRestorationType?,
|
||||
onOptionSelected: (BackupRestorationType) -> Unit
|
||||
) {
|
||||
Option(
|
||||
icon = {
|
||||
Box(
|
||||
modifier = Modifier.padding(horizontal = 18.dp)
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.symbol_backup_light), // TODO [message-backups] Finalized asset.
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.size(36.dp)
|
||||
)
|
||||
}
|
||||
},
|
||||
isSelected = selectedOption == BackupRestorationType.NONE,
|
||||
title = "Log in without transferring", // TODO [message-backups] Finalized copy.
|
||||
subtitle = "Continue without transferring your messages and media", // TODO [message-backups] Finalized copy.
|
||||
onClick = { onOptionSelected(BackupRestorationType.NONE) }
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun TransferFromAndroidDeviceOptionPreview() {
|
||||
Previews.BottomSheetPreview {
|
||||
TransferFromAndroidDeviceOption(
|
||||
selectedOption = null,
|
||||
onOptionSelected = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TransferFromAndroidDeviceOption(
|
||||
selectedOption: BackupRestorationType?,
|
||||
onOptionSelected: (BackupRestorationType) -> Unit
|
||||
) {
|
||||
Option(
|
||||
icon = {
|
||||
Box(
|
||||
modifier = Modifier.padding(horizontal = 18.dp)
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.symbol_backup_light), // TODO [message-backups] Finalized asset.
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.size(36.dp)
|
||||
)
|
||||
}
|
||||
},
|
||||
isSelected = selectedOption == BackupRestorationType.DEVICE_TRANSFER,
|
||||
title = "Transfer from Android device", // TODO [message-backups] Finalized copy.
|
||||
subtitle = "Transfer your account and messages from your old device.", // TODO [message-backups] Finalized copy.
|
||||
onClick = { onOptionSelected(BackupRestorationType.DEVICE_TRANSFER) }
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun RestoreLocalBackupOptionPreview() {
|
||||
Previews.BottomSheetPreview {
|
||||
RestoreLocalBackupOption(
|
||||
selectedOption = null,
|
||||
onOptionSelected = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RestoreLocalBackupOption(
|
||||
selectedOption: BackupRestorationType?,
|
||||
onOptionSelected: (BackupRestorationType) -> Unit
|
||||
) {
|
||||
Option(
|
||||
icon = {
|
||||
Box(
|
||||
modifier = Modifier.padding(horizontal = 18.dp)
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.symbol_backup_light), // TODO [message-backups] Finalized asset.
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.size(36.dp)
|
||||
)
|
||||
}
|
||||
},
|
||||
isSelected = selectedOption == BackupRestorationType.LOCAL_BACKUP,
|
||||
title = "Restore local backup", // TODO [message-backups] Finalized copy.
|
||||
subtitle = "Restore your messages from a backup file you saved on your device.", // TODO [message-backups] Finalized copy.
|
||||
onClick = { onOptionSelected(BackupRestorationType.LOCAL_BACKUP) }
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun OptionPreview() {
|
||||
Previews.BottomSheetPreview {
|
||||
Option(
|
||||
icon = {
|
||||
Box(
|
||||
modifier = Modifier.padding(horizontal = 18.dp)
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.symbol_backup_light), // TODO [message-backups] Finalized asset.
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.size(36.dp)
|
||||
)
|
||||
}
|
||||
},
|
||||
isSelected = false,
|
||||
title = "Log in without transferring", // TODO [message-backups] Finalized copy.
|
||||
subtitle = "Continue without transferring your messages and media", // TODO [message-backups] Finalized copy.
|
||||
onClick = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Option(
|
||||
icon: @Composable () -> Unit,
|
||||
isSelected: Boolean,
|
||||
title: String,
|
||||
subtitle: String,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.background(
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
)
|
||||
.border(
|
||||
width = if (isSelected) 2.dp else 0.dp,
|
||||
color = if (isSelected) MaterialTheme.colorScheme.primary else Color.Transparent
|
||||
)
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.clickable { onClick() }
|
||||
.padding(vertical = 21.dp)
|
||||
) {
|
||||
icon()
|
||||
Column {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
Text(
|
||||
text = subtitle,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.devicetransfer.newdevice
|
||||
|
||||
/**
|
||||
* What kind of backup restore the user wishes to perform.
|
||||
*/
|
||||
enum class BackupRestorationType {
|
||||
DEVICE_TRANSFER,
|
||||
LOCAL_BACKUP,
|
||||
REMOTE_BACKUP,
|
||||
NONE
|
||||
}
|
|
@ -2,14 +2,17 @@ package org.thoughtcrime.securesms.devicetransfer.newdevice;
|
|||
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.navigation.Navigation;
|
||||
|
||||
import org.signal.core.util.concurrent.LifecycleDisposable;
|
||||
import org.thoughtcrime.securesms.LoggingFragment;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.databinding.FragmentTransferRestoreBinding;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.SpanUtil;
|
||||
import org.thoughtcrime.securesms.util.navigation.SafeNavigation;
|
||||
|
||||
|
@ -18,22 +21,51 @@ import org.thoughtcrime.securesms.util.navigation.SafeNavigation;
|
|||
*/
|
||||
public final class TransferOrRestoreFragment extends LoggingFragment {
|
||||
|
||||
private final LifecycleDisposable lifecycleDisposable = new LifecycleDisposable();
|
||||
|
||||
private FragmentTransferRestoreBinding binding;
|
||||
|
||||
public TransferOrRestoreFragment() {
|
||||
super(R.layout.fragment_transfer_restore);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
view.findViewById(R.id.transfer_or_restore_fragment_transfer)
|
||||
.setOnClickListener(v -> SafeNavigation.safeNavigate(Navigation.findNavController(v), R.id.action_new_device_transfer_instructions));
|
||||
binding = FragmentTransferRestoreBinding.bind(view);
|
||||
|
||||
View restoreBackup = view.findViewById(R.id.transfer_or_restore_fragment_restore);
|
||||
restoreBackup.setOnClickListener(v -> SafeNavigation.safeNavigate(Navigation.findNavController(v), R.id.action_choose_backup));
|
||||
TransferOrRestoreViewModel viewModel = new ViewModelProvider(this).get(TransferOrRestoreViewModel.class);
|
||||
|
||||
binding.transferOrRestoreFragmentTransfer.setOnClickListener(v -> viewModel.onTransferFromAndroidDeviceSelected());
|
||||
binding.transferOrRestoreFragmentRestore.setOnClickListener(v -> viewModel.onRestoreFromLocalBackupSelected());
|
||||
binding.transferOrRestoreFragmentRestoreRemote.setOnClickListener(v -> viewModel.onRestoreFromRemoteBackupSelected());
|
||||
binding.transferOrRestoreFragmentNext.setOnClickListener(v -> launchSelection(viewModel.getStateSnapshot()));
|
||||
binding.transferOrRestoreFragmentMoreOptions.setOnClickListener(v -> SafeNavigation.safeNavigate(Navigation.findNavController(requireView()), R.id.action_transferOrRestore_to_moreOptions));
|
||||
|
||||
int visibility = FeatureFlags.messageBackups() ? View.VISIBLE : View.GONE;
|
||||
binding.transferOrRestoreFragmentRestoreRemoteCard.setVisibility(visibility);
|
||||
binding.transferOrRestoreFragmentMoreOptions.setVisibility(visibility);
|
||||
|
||||
String description = getString(R.string.TransferOrRestoreFragment__transfer_your_account_and_messages_from_your_old_android_device);
|
||||
String toBold = getString(R.string.TransferOrRestoreFragment__you_need_access_to_your_old_device);
|
||||
|
||||
TextView transferDescriptionView = view.findViewById(R.id.transfer_or_restore_fragment_transfer_description);
|
||||
transferDescriptionView.setText(SpanUtil.boldSubstring(description, toBold));
|
||||
binding.transferOrRestoreFragmentTransferDescription.setText(SpanUtil.boldSubstring(description, toBold));
|
||||
|
||||
lifecycleDisposable.bindTo(getViewLifecycleOwner());
|
||||
lifecycleDisposable.add(viewModel.getState().subscribe(this::updateSelection));
|
||||
}
|
||||
|
||||
private void updateSelection(BackupRestorationType restorationType) {
|
||||
binding.transferOrRestoreFragmentTransferCard.setSelected(restorationType == BackupRestorationType.DEVICE_TRANSFER);
|
||||
binding.transferOrRestoreFragmentRestoreCard.setSelected(restorationType == BackupRestorationType.LOCAL_BACKUP);
|
||||
binding.transferOrRestoreFragmentRestoreRemoteCard.setSelected(restorationType == BackupRestorationType.REMOTE_BACKUP);
|
||||
}
|
||||
|
||||
private void launchSelection(BackupRestorationType restorationType) {
|
||||
switch (restorationType) {
|
||||
case DEVICE_TRANSFER -> SafeNavigation.safeNavigate(Navigation.findNavController(requireView()), R.id.action_new_device_transfer_instructions);
|
||||
case LOCAL_BACKUP -> SafeNavigation.safeNavigate(Navigation.findNavController(requireView()), R.id.action_choose_backup);
|
||||
case REMOTE_BACKUP -> {}
|
||||
default -> throw new IllegalArgumentException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.devicetransfer.newdevice
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import io.reactivex.rxjava3.processors.BehaviorProcessor
|
||||
|
||||
/**
|
||||
* Maintains state of the TransferOrRestoreFragment
|
||||
*/
|
||||
class TransferOrRestoreViewModel : ViewModel() {
|
||||
|
||||
private val internalState = BehaviorProcessor.createDefault(BackupRestorationType.DEVICE_TRANSFER)
|
||||
|
||||
val state: Flowable<BackupRestorationType> = internalState.distinctUntilChanged().observeOn(AndroidSchedulers.mainThread())
|
||||
val stateSnapshot: BackupRestorationType get() = internalState.value!!
|
||||
|
||||
fun onTransferFromAndroidDeviceSelected() {
|
||||
internalState.onNext(BackupRestorationType.DEVICE_TRANSFER)
|
||||
}
|
||||
|
||||
fun onRestoreFromLocalBackupSelected() {
|
||||
internalState.onNext(BackupRestorationType.LOCAL_BACKUP)
|
||||
}
|
||||
|
||||
fun onRestoreFromRemoteBackupSelected() {
|
||||
internalState.onNext(BackupRestorationType.REMOTE_BACKUP)
|
||||
}
|
||||
}
|
|
@ -67,7 +67,7 @@ final class OldDeviceClientTask implements ClientTask {
|
|||
|
||||
@Override
|
||||
public void success() {
|
||||
SignalStore.misc().markOldDeviceTransferLocked();
|
||||
SignalStore.misc().setOldDeviceTransferLocked(true);
|
||||
EventBus.getDefault().post(new Status(0, 0, 0,true));
|
||||
}
|
||||
|
||||
|
|
|
@ -47,7 +47,10 @@ class WebRtcViewModel(state: WebRtcServiceState) {
|
|||
get() = this == CALL_PRE_JOIN || this == NETWORK_FAILURE
|
||||
|
||||
val isPassedPreJoin: Boolean
|
||||
get() = ordinal > ordinal
|
||||
get() = ordinal > CALL_PRE_JOIN.ordinal
|
||||
|
||||
val inOngoingCall: Boolean
|
||||
get() = this == CALL_INCOMING || this == CALL_OUTGOING || this == CALL_CONNECTED || this == CALL_RINGING || this == CALL_RECONNECTING
|
||||
}
|
||||
|
||||
enum class GroupCallState {
|
||||
|
|
|
@ -118,6 +118,14 @@ public abstract class GroupId implements DatabaseId {
|
|||
}
|
||||
}
|
||||
|
||||
public static GroupId.Push pushOrNull(byte[] bytes) {
|
||||
try {
|
||||
return GroupId.push(bytes);
|
||||
} catch (BadGroupIdException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static @NonNull GroupId parseOrThrow(@NonNull String encodedGroupId) {
|
||||
try {
|
||||
return parse(encodedGroupId);
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.jobmanager
|
||||
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.BackoffUtil
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||
|
||||
/**
|
||||
* Helper to calculate the default backoff interval for a [Job] given it's run attempt count.
|
||||
*/
|
||||
fun Job.defaultBackoffInterval(): Long = BackoffUtil.exponentialBackoff(runAttempt + 1, FeatureFlags.getDefaultMaxBackoff())
|
|
@ -205,7 +205,7 @@ public final class AttachmentCompressionJob extends BaseJob {
|
|||
} else if (constraints.canResize(attachment)) {
|
||||
Log.i(TAG, "Compressing image.");
|
||||
try (MediaStream converted = compressImage(context, attachment, constraints)) {
|
||||
attachmentDatabase.updateAttachmentData(attachment, converted, false);
|
||||
attachmentDatabase.updateAttachmentData(attachment, converted);
|
||||
}
|
||||
attachmentDatabase.markAttachmentAsTransformed(attachmentId, false);
|
||||
} else if (constraints.isSatisfied(context, attachment)) {
|
||||
|
@ -263,7 +263,7 @@ public final class AttachmentCompressionJob extends BaseJob {
|
|||
Log.i(TAG, "Compressing with streaming muxer");
|
||||
AttachmentSecret attachmentSecret = AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret();
|
||||
|
||||
File file = AttachmentTable.newFile(context);
|
||||
File file = AttachmentTable.newDataFile(context);
|
||||
file.deleteOnExit();
|
||||
|
||||
boolean faststart = false;
|
||||
|
@ -296,7 +296,7 @@ public final class AttachmentCompressionJob extends BaseJob {
|
|||
|
||||
final long plaintextLength = ModernEncryptingPartOutputStream.getPlaintextLength(file.length());
|
||||
try (MediaStream mediaStream = new MediaStream(postProcessor.process(plaintextLength), MimeTypes.VIDEO_MP4, 0, 0, true)) {
|
||||
attachmentDatabase.updateAttachmentData(attachment, mediaStream, true);
|
||||
attachmentDatabase.updateAttachmentData(attachment, mediaStream);
|
||||
faststart = true;
|
||||
} catch (VideoPostProcessingException e) {
|
||||
Log.w(TAG, "Exception thrown during post processing.", e);
|
||||
|
@ -310,7 +310,7 @@ public final class AttachmentCompressionJob extends BaseJob {
|
|||
|
||||
if (!faststart) {
|
||||
try (MediaStream mediaStream = new MediaStream(ModernDecryptingPartInputStream.createFor(attachmentSecret, file, 0), MimeTypes.VIDEO_MP4, 0, 0, false)) {
|
||||
attachmentDatabase.updateAttachmentData(attachment, mediaStream, true);
|
||||
attachmentDatabase.updateAttachmentData(attachment, mediaStream);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
|
@ -339,7 +339,7 @@ public final class AttachmentCompressionJob extends BaseJob {
|
|||
100,
|
||||
percent));
|
||||
}, cancelationSignal)) {
|
||||
attachmentDatabase.updateAttachmentData(attachment, mediaStream, true);
|
||||
attachmentDatabase.updateAttachmentData(attachment, mediaStream);
|
||||
attachmentDatabase.markAttachmentAsTransformed(attachment.attachmentId, mediaStream.getFaststart());
|
||||
}
|
||||
|
||||
|
|
|
@ -168,7 +168,7 @@ public final class AttachmentDownloadJob extends BaseJob {
|
|||
if (attachment.cdnNumber != ReleaseChannel.CDN_NUMBER) {
|
||||
retrieveAttachment(messageId, attachmentId, attachment);
|
||||
} else {
|
||||
retrieveUrlAttachment(messageId, attachmentId, attachment);
|
||||
retrieveAttachmentForReleaseChannel(messageId, attachmentId, attachment);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -216,7 +216,7 @@ public final class AttachmentDownloadJob extends BaseJob {
|
|||
return isCanceled();
|
||||
}
|
||||
});
|
||||
database.insertAttachmentsForPlaceholder(messageId, attachmentId, stream);
|
||||
database.finalizeAttachmentAfterDownload(messageId, attachmentId, stream);
|
||||
} catch (RangeException e) {
|
||||
Log.w(TAG, "Range exception, file size " + attachmentFile.length(), e);
|
||||
if (attachmentFile.delete()) {
|
||||
|
@ -278,9 +278,9 @@ public final class AttachmentDownloadJob extends BaseJob {
|
|||
}
|
||||
}
|
||||
|
||||
private void retrieveUrlAttachment(long messageId,
|
||||
final AttachmentId attachmentId,
|
||||
final Attachment attachment)
|
||||
private void retrieveAttachmentForReleaseChannel(long messageId,
|
||||
final AttachmentId attachmentId,
|
||||
final Attachment attachment)
|
||||
throws IOException
|
||||
{
|
||||
try (Response response = S3.getObject(Objects.requireNonNull(attachment.fileName))) {
|
||||
|
@ -289,7 +289,7 @@ public final class AttachmentDownloadJob extends BaseJob {
|
|||
if (body.contentLength() > FeatureFlags.maxAttachmentReceiveSizeBytes()) {
|
||||
throw new MmsException("Attachment too large, failing download");
|
||||
}
|
||||
SignalDatabase.attachments().insertAttachmentsForPlaceholder(messageId, attachmentId, Okio.buffer(body.source()).inputStream());
|
||||
SignalDatabase.attachments().finalizeAttachmentAfterDownload(messageId, attachmentId, Okio.buffer(body.source()).inputStream());
|
||||
}
|
||||
} catch (MmsException e) {
|
||||
Log.w(TAG, "Experienced exception while trying to download an attachment.", e);
|
||||
|
|
|
@ -0,0 +1,112 @@
|
|||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.jobs
|
||||
|
||||
import org.signal.core.util.ThreadUtil
|
||||
import org.signal.core.util.drain
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.attachments.AttachmentId
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.jobmanager.Job
|
||||
import org.thoughtcrime.securesms.jobmanager.defaultBackoffInterval
|
||||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.IOException
|
||||
import java.security.DigestInputStream
|
||||
import java.security.MessageDigest
|
||||
|
||||
/**
|
||||
* This job backfills hashes for attachments that were sent before we started hashing them.
|
||||
* In order to avoid hammering the device with hash calculations and disk I/O, this job will
|
||||
* calculate the hash for a single attachment and then reschedule itself to run again if necessary.
|
||||
*/
|
||||
class AttachmentHashBackfillJob private constructor(parameters: Parameters) : Job(parameters) {
|
||||
|
||||
companion object {
|
||||
val TAG = Log.tag(AttachmentHashBackfillJob::class.java)
|
||||
|
||||
const val KEY = "AttachmentHashBackfillJob"
|
||||
}
|
||||
|
||||
private var activeFile: File? = null
|
||||
|
||||
constructor() : this(
|
||||
Parameters.Builder()
|
||||
.setQueue(KEY)
|
||||
.setMaxInstancesForFactory(2)
|
||||
.setLifespan(Parameters.IMMORTAL)
|
||||
.setMaxAttempts(10)
|
||||
.build()
|
||||
)
|
||||
|
||||
override fun serialize() = null
|
||||
|
||||
override fun getFactoryKey() = KEY
|
||||
|
||||
override fun run(): Result {
|
||||
val (file: File?, attachmentId: AttachmentId?) = SignalDatabase.attachments.getUnhashedDataFile() ?: (null to null)
|
||||
if (file == null || attachmentId == null) {
|
||||
Log.i(TAG, "No more unhashed files! Task complete.")
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
activeFile = file
|
||||
|
||||
if (!file.exists()) {
|
||||
Log.w(TAG, "File does not exist! Clearing all usages.", true)
|
||||
SignalDatabase.attachments.clearUsagesOfDataFile(file)
|
||||
ApplicationDependencies.getJobManager().add(AttachmentHashBackfillJob())
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
try {
|
||||
val inputStream = SignalDatabase.attachments.getAttachmentStream(attachmentId, 0)
|
||||
val messageDigest = MessageDigest.getInstance("SHA-256")
|
||||
|
||||
DigestInputStream(inputStream, messageDigest).use {
|
||||
it.drain()
|
||||
}
|
||||
|
||||
val hash = messageDigest.digest()
|
||||
|
||||
SignalDatabase.attachments.setHashForDataFile(file, hash)
|
||||
} catch (e: FileNotFoundException) {
|
||||
Log.w(TAG, "File could not be found! Clearing all usages.", true)
|
||||
SignalDatabase.attachments.clearUsagesOfDataFile(file)
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "Error hashing attachment. Retrying.", e)
|
||||
|
||||
if (e.cause is FileNotFoundException) {
|
||||
Log.w(TAG, "Underlying cause was a FileNotFoundException. Clearing all usages.", true)
|
||||
SignalDatabase.attachments.clearUsagesOfDataFile(file)
|
||||
} else {
|
||||
return Result.retry(defaultBackoffInterval())
|
||||
}
|
||||
}
|
||||
|
||||
// Sleep just so we don't hammer the device with hash calculations and disk I/O
|
||||
ThreadUtil.sleep(1000)
|
||||
|
||||
ApplicationDependencies.getJobManager().add(AttachmentHashBackfillJob())
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
override fun onFailure() {
|
||||
activeFile?.let { file ->
|
||||
Log.w(TAG, "Failed to calculate hash, marking as unhashable: $file", true)
|
||||
SignalDatabase.attachments.markDataFileAsUnhashable(file)
|
||||
} ?: Log.w(TAG, "Job failed, but no active file is set!")
|
||||
|
||||
ApplicationDependencies.getJobManager().add(AttachmentHashBackfillJob())
|
||||
}
|
||||
|
||||
class Factory : Job.Factory<AttachmentHashBackfillJob> {
|
||||
override fun create(parameters: Parameters, serializedData: ByteArray?): AttachmentHashBackfillJob {
|
||||
return AttachmentHashBackfillJob(parameters)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -42,6 +42,7 @@ import java.io.IOException
|
|||
import java.util.Objects
|
||||
import java.util.Optional
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.time.Duration.Companion.days
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
/**
|
||||
|
@ -60,7 +61,7 @@ class AttachmentUploadJob private constructor(
|
|||
|
||||
private val TAG = Log.tag(AttachmentUploadJob::class.java)
|
||||
|
||||
private val UPLOAD_REUSE_THRESHOLD = TimeUnit.DAYS.toMillis(3)
|
||||
val UPLOAD_REUSE_THRESHOLD = 3.days.inWholeMilliseconds
|
||||
|
||||
/**
|
||||
* Foreground notification shows while uploading attachments above this.
|
||||
|
@ -162,7 +163,7 @@ class AttachmentUploadJob private constructor(
|
|||
buildAttachmentStream(databaseAttachment, notification, uploadSpec!!).use { localAttachment ->
|
||||
val remoteAttachment = messageSender.uploadAttachment(localAttachment)
|
||||
val attachment = PointerAttachment.forPointer(Optional.of(remoteAttachment), null, databaseAttachment.fastPreflightId).get()
|
||||
SignalDatabase.attachments.updateAttachmentAfterUpload(databaseAttachment.attachmentId, attachment, remoteAttachment.uploadTimestamp)
|
||||
SignalDatabase.attachments.finalizeAttachmentAfterUpload(databaseAttachment.attachmentId, attachment, remoteAttachment.uploadTimestamp)
|
||||
}
|
||||
}
|
||||
} catch (e: NonSuccessfulResumableUploadResponseCodeException) {
|
||||
|
|
|
@ -5,10 +5,14 @@
|
|||
|
||||
package org.thoughtcrime.securesms.jobs
|
||||
|
||||
import androidx.annotation.WorkerThread
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.thoughtcrime.securesms.database.CallTable
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.jobmanager.Job
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
|
||||
import org.thoughtcrime.securesms.jobs.protos.CallLogEventSendJobData
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage
|
||||
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException
|
||||
import org.whispersystems.signalservice.api.push.exceptions.ServerRejectedException
|
||||
|
@ -27,8 +31,9 @@ class CallLogEventSendJob private constructor(
|
|||
companion object {
|
||||
const val KEY = "CallLogEventSendJob"
|
||||
|
||||
@WorkerThread
|
||||
fun forClearHistory(
|
||||
timestamp: Long
|
||||
call: CallTable.Call
|
||||
) = CallLogEventSendJob(
|
||||
Parameters.Builder()
|
||||
.setQueue("CallLogEventSendJob")
|
||||
|
@ -37,13 +42,16 @@ class CallLogEventSendJob private constructor(
|
|||
.addConstraint(NetworkConstraint.KEY)
|
||||
.build(),
|
||||
SyncMessage.CallLogEvent(
|
||||
timestamp = timestamp,
|
||||
timestamp = call.timestamp,
|
||||
callId = call.callId,
|
||||
conversationId = Recipient.resolved(call.peer).requireCallConversationId().toByteString(),
|
||||
type = SyncMessage.CallLogEvent.Type.CLEAR
|
||||
)
|
||||
)
|
||||
|
||||
@WorkerThread
|
||||
fun forMarkedAsRead(
|
||||
timestamp: Long
|
||||
call: CallTable.Call
|
||||
) = CallLogEventSendJob(
|
||||
Parameters.Builder()
|
||||
.setQueue("CallLogEventSendJob")
|
||||
|
@ -52,7 +60,9 @@ class CallLogEventSendJob private constructor(
|
|||
.addConstraint(NetworkConstraint.KEY)
|
||||
.build(),
|
||||
SyncMessage.CallLogEvent(
|
||||
timestamp = timestamp,
|
||||
timestamp = call.timestamp,
|
||||
callId = call.callId,
|
||||
conversationId = Recipient.resolved(call.peer).requireCallConversationId().toByteString(),
|
||||
type = SyncMessage.CallLogEvent.Type.MARKED_AS_READ
|
||||
)
|
||||
)
|
||||
|
|
|
@ -38,6 +38,7 @@ import org.thoughtcrime.securesms.migrations.AccountConsistencyMigrationJob;
|
|||
import org.thoughtcrime.securesms.migrations.AccountRecordMigrationJob;
|
||||
import org.thoughtcrime.securesms.migrations.ApplyUnknownFieldsToSelfMigrationJob;
|
||||
import org.thoughtcrime.securesms.migrations.AttachmentCleanupMigrationJob;
|
||||
import org.thoughtcrime.securesms.migrations.AttachmentHashBackfillMigrationJob;
|
||||
import org.thoughtcrime.securesms.migrations.AttributesMigrationJob;
|
||||
import org.thoughtcrime.securesms.migrations.AvatarIdRemovalMigrationJob;
|
||||
import org.thoughtcrime.securesms.migrations.BackupJitterMigrationJob;
|
||||
|
@ -95,6 +96,7 @@ public final class JobManagerFactories {
|
|||
put(AttachmentCompressionJob.KEY, new AttachmentCompressionJob.Factory());
|
||||
put(AttachmentCopyJob.KEY, new AttachmentCopyJob.Factory());
|
||||
put(AttachmentDownloadJob.KEY, new AttachmentDownloadJob.Factory());
|
||||
put(AttachmentHashBackfillJob.KEY, new AttachmentHashBackfillJob.Factory());
|
||||
put(AttachmentMarkUploadedJob.KEY, new AttachmentMarkUploadedJob.Factory());
|
||||
put(AttachmentUploadJob.KEY, new AttachmentUploadJob.Factory());
|
||||
put(AutomaticSessionResetJob.KEY, new AutomaticSessionResetJob.Factory());
|
||||
|
@ -130,6 +132,7 @@ public final class JobManagerFactories {
|
|||
put(LeaveGroupV2Job.KEY, new LeaveGroupV2Job.Factory());
|
||||
put(LeaveGroupV2WorkerJob.KEY, new LeaveGroupV2WorkerJob.Factory());
|
||||
put(LegacyAttachmentUploadJob.KEY, new LegacyAttachmentUploadJob.Factory());
|
||||
put(LinkedDeviceInactiveCheckJob.KEY, new LinkedDeviceInactiveCheckJob.Factory());
|
||||
put(LocalBackupJob.KEY, new LocalBackupJob.Factory());
|
||||
put(LocalBackupJobApi29.KEY, new LocalBackupJobApi29.Factory());
|
||||
put(MarkerJob.KEY, new MarkerJob.Factory());
|
||||
|
@ -219,6 +222,7 @@ public final class JobManagerFactories {
|
|||
put(AccountRecordMigrationJob.KEY, new AccountRecordMigrationJob.Factory());
|
||||
put(ApplyUnknownFieldsToSelfMigrationJob.KEY, new ApplyUnknownFieldsToSelfMigrationJob.Factory());
|
||||
put(AttachmentCleanupMigrationJob.KEY, new AttachmentCleanupMigrationJob.Factory());
|
||||
put(AttachmentHashBackfillMigrationJob.KEY, new AttachmentHashBackfillMigrationJob.Factory());
|
||||
put(AttributesMigrationJob.KEY, new AttributesMigrationJob.Factory());
|
||||
put(AvatarIdRemovalMigrationJob.KEY, new AvatarIdRemovalMigrationJob.Factory());
|
||||
put("AvatarMigrationJob", new FailingJob.Factory());
|
||||
|
|
|
@ -138,7 +138,7 @@ public final class LegacyAttachmentUploadJob extends BaseJob {
|
|||
SignalServiceAttachmentPointer remoteAttachment = messageSender.uploadAttachment(localAttachment);
|
||||
Attachment attachment = PointerAttachment.forPointer(Optional.of(remoteAttachment), null, databaseAttachment.fastPreflightId).get();
|
||||
|
||||
database.updateAttachmentAfterUpload(databaseAttachment.attachmentId, attachment, remoteAttachment.getUploadTimestamp());
|
||||
database.finalizeAttachmentAfterUpload(databaseAttachment.attachmentId, attachment, remoteAttachment.getUploadTimestamp());
|
||||
}
|
||||
} catch (NonSuccessfulResumableUploadResponseCodeException e) {
|
||||
if (e.getCode() == 400) {
|
||||
|
|
|
@ -0,0 +1,117 @@
|
|||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.jobs
|
||||
|
||||
import org.signal.core.util.Base64
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.roundedString
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.devicelist.protos.DeviceName
|
||||
import org.thoughtcrime.securesms.jobmanager.Job
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.keyvalue.protos.LeastActiveLinkedDevice
|
||||
import org.thoughtcrime.securesms.registration.secondary.DeviceNameCipher
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress
|
||||
import java.io.IOException
|
||||
import kotlin.time.Duration.Companion.days
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.DurationUnit
|
||||
|
||||
/**
|
||||
* Designed as a routine check to keep an eye on how active our linked devices are.
|
||||
*/
|
||||
class LinkedDeviceInactiveCheckJob private constructor(
|
||||
parameters: Parameters = Parameters.Builder()
|
||||
.setQueue("LinkedDeviceInactiveCheckJob")
|
||||
.setMaxInstancesForFactory(2)
|
||||
.setLifespan(30.days.inWholeMilliseconds)
|
||||
.setMaxAttempts(Parameters.UNLIMITED)
|
||||
.addConstraint(NetworkConstraint.KEY)
|
||||
.build()
|
||||
) : Job(parameters) {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(LinkedDeviceInactiveCheckJob::class.java)
|
||||
const val KEY = "LinkedDeviceInactiveCheckJob"
|
||||
|
||||
@JvmStatic
|
||||
fun enqueue() {
|
||||
ApplicationDependencies.getJobManager().add(LinkedDeviceInactiveCheckJob())
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun enqueueIfNecessary() {
|
||||
val timeSinceLastCheck = System.currentTimeMillis() - SignalStore.misc().linkedDeviceLastActiveCheckTime
|
||||
if (timeSinceLastCheck > 1.days.inWholeMilliseconds || timeSinceLastCheck < 0) {
|
||||
ApplicationDependencies.getJobManager().add(LinkedDeviceInactiveCheckJob())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun serialize(): ByteArray? = null
|
||||
|
||||
override fun getFactoryKey(): String = KEY
|
||||
|
||||
override fun run(): Result {
|
||||
val devices = try {
|
||||
ApplicationDependencies.getSignalServiceAccountManager().devices
|
||||
} catch (e: IOException) {
|
||||
return Result.retry(defaultBackoff())
|
||||
}
|
||||
|
||||
if (devices.isEmpty()) {
|
||||
Log.i(TAG, "No linked devices found.")
|
||||
|
||||
SignalStore.misc().hasLinkedDevices = false
|
||||
SignalStore.misc().leastActiveLinkedDevice = null
|
||||
SignalStore.misc().linkedDeviceLastActiveCheckTime = System.currentTimeMillis()
|
||||
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
val leastActiveDevice: LeastActiveLinkedDevice? = devices
|
||||
.filter { it.id != SignalServiceAddress.DEFAULT_DEVICE_ID }
|
||||
.filter { it.name != null }
|
||||
.minBy { it.lastSeen }
|
||||
.let {
|
||||
val nameProto = DeviceName.ADAPTER.decode(Base64.decode(it.getName()))
|
||||
val decryptedBytes = DeviceNameCipher.decryptDeviceName(nameProto, ApplicationDependencies.getProtocolStore().aci().identityKeyPair) ?: return@let null
|
||||
val name = String(decryptedBytes)
|
||||
|
||||
LeastActiveLinkedDevice(
|
||||
name = name,
|
||||
lastActiveTimestamp = it.lastSeen
|
||||
)
|
||||
}
|
||||
|
||||
if (leastActiveDevice == null) {
|
||||
Log.w(TAG, "Failed to decrypt linked device name.")
|
||||
SignalStore.misc().hasLinkedDevices = true
|
||||
SignalStore.misc().leastActiveLinkedDevice = null
|
||||
SignalStore.misc().linkedDeviceLastActiveCheckTime = System.currentTimeMillis()
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
val timeSinceActive = System.currentTimeMillis() - leastActiveDevice.lastActiveTimestamp
|
||||
Log.i(TAG, "Least active linked device was last active ${timeSinceActive.milliseconds.toDouble(DurationUnit.DAYS).roundedString(2)} days ago ($timeSinceActive ms).")
|
||||
|
||||
SignalStore.misc().hasLinkedDevices = true
|
||||
SignalStore.misc().leastActiveLinkedDevice = leastActiveDevice
|
||||
SignalStore.misc().linkedDeviceLastActiveCheckTime = System.currentTimeMillis()
|
||||
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
override fun onFailure() {
|
||||
}
|
||||
|
||||
class Factory : Job.Factory<LinkedDeviceInactiveCheckJob> {
|
||||
override fun create(parameters: Parameters, serializedData: ByteArray?): LinkedDeviceInactiveCheckJob {
|
||||
return LinkedDeviceInactiveCheckJob(parameters)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -122,8 +122,6 @@ class MultiDeviceContactSyncJob(parameters: Parameters, private val attachmentPo
|
|||
}
|
||||
}
|
||||
|
||||
recipients.setBlocked(recipient.id, contact.isBlocked)
|
||||
|
||||
val threadRecord = threads.getThreadRecord(threads.getThreadIdFor(recipient.id))
|
||||
if (threadRecord != null && contact.isArchived != threadRecord.isArchived) {
|
||||
if (contact.isArchived) {
|
||||
|
|
|
@ -168,7 +168,6 @@ public class MultiDeviceContactUpdateJob extends BaseJob {
|
|||
Optional.of(ChatColorsMapper.getMaterialColor(recipient.getChatColors()).serialize()),
|
||||
verifiedMessage,
|
||||
ProfileKeyUtil.profileKeyOptional(recipient.getProfileKey()),
|
||||
recipient.isBlocked(),
|
||||
recipient.getExpiresInSeconds() > 0 ? Optional.of(recipient.getExpiresInSeconds())
|
||||
: Optional.empty(),
|
||||
Optional.ofNullable(inboxPositions.get(recipientId)),
|
||||
|
@ -235,7 +234,6 @@ public class MultiDeviceContactUpdateJob extends BaseJob {
|
|||
Optional.of(ChatColorsMapper.getMaterialColor(recipient.getChatColors()).serialize()),
|
||||
verified,
|
||||
profileKey,
|
||||
blocked,
|
||||
expireTimer,
|
||||
inboxPosition,
|
||||
archived.contains(recipient.getId())));
|
||||
|
@ -253,7 +251,6 @@ public class MultiDeviceContactUpdateJob extends BaseJob {
|
|||
Optional.of(ChatColorsMapper.getMaterialColor(self.getChatColors()).serialize()),
|
||||
Optional.empty(),
|
||||
ProfileKeyUtil.profileKeyOptionalOrThrow(self.getProfileKey()),
|
||||
false,
|
||||
self.getExpiresInSeconds() > 0 ? Optional.of(self.getExpiresInSeconds()) : Optional.empty(),
|
||||
Optional.ofNullable(inboxPositions.get(self.getId())),
|
||||
archived.contains(self.getId())));
|
||||
|
|
|
@ -84,7 +84,6 @@ public class MultiDeviceProfileKeyUpdateJob extends BaseJob {
|
|||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
profileKey,
|
||||
false,
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
false));
|
||||
|
|
|
@ -48,7 +48,7 @@ class PnpInitializeDevicesJob private constructor(parameters: Parameters) : Base
|
|||
|
||||
@JvmStatic
|
||||
fun enqueueIfNecessary() {
|
||||
if (SignalStore.misc().hasPniInitializedDevices() || !SignalStore.account().isRegistered || SignalStore.account().aci == null) {
|
||||
if (SignalStore.misc().hasPniInitializedDevices || !SignalStore.account().isRegistered || SignalStore.account().aci == null) {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -77,19 +77,19 @@ class PnpInitializeDevicesJob private constructor(parameters: Parameters) : Base
|
|||
|
||||
if (!TextSecurePreferences.isMultiDevice(context)) {
|
||||
Log.i(TAG, "Not multi device, aborting...")
|
||||
SignalStore.misc().setPniInitializedDevices(true)
|
||||
SignalStore.misc().hasPniInitializedDevices = true
|
||||
return
|
||||
}
|
||||
|
||||
if (SignalStore.account().isLinkedDevice) {
|
||||
Log.i(TAG, "Not primary device, aborting...")
|
||||
SignalStore.misc().setPniInitializedDevices(true)
|
||||
SignalStore.misc().hasPniInitializedDevices = true
|
||||
return
|
||||
}
|
||||
|
||||
ChangeNumberRepository.CHANGE_NUMBER_LOCK.lock()
|
||||
try {
|
||||
if (SignalStore.misc().hasPniInitializedDevices()) {
|
||||
if (SignalStore.misc().hasPniInitializedDevices) {
|
||||
Log.w(TAG, "We found out that things have been initialized after we got the lock! No need to do anything else.")
|
||||
return
|
||||
}
|
||||
|
@ -110,7 +110,7 @@ class PnpInitializeDevicesJob private constructor(parameters: Parameters) : Base
|
|||
throw t
|
||||
}
|
||||
|
||||
SignalStore.misc().setPniInitializedDevices(true)
|
||||
SignalStore.misc().hasPniInitializedDevices = true
|
||||
} finally {
|
||||
ChangeNumberRepository.CHANGE_NUMBER_LOCK.unlock()
|
||||
}
|
||||
|
|
|
@ -124,13 +124,18 @@ class PreKeysSyncJob private constructor(
|
|||
}
|
||||
|
||||
val forceRotation = if (forceRotationRequested) {
|
||||
warn(TAG, "Forced rotation was requested.")
|
||||
warn(TAG, ServiceIdType.ACI, "Active Signed EC: ${SignalStore.account().aciPreKeys.activeSignedPreKeyId}, Last Resort Kyber: ${SignalStore.account().aciPreKeys.lastResortKyberPreKeyId}")
|
||||
warn(TAG, ServiceIdType.PNI, "Active Signed EC: ${SignalStore.account().pniPreKeys.activeSignedPreKeyId}, Last Resort Kyber: ${SignalStore.account().pniPreKeys.lastResortKyberPreKeyId}")
|
||||
|
||||
if (!checkPreKeyConsistency(ServiceIdType.ACI, ApplicationDependencies.getProtocolStore().aci(), SignalStore.account().aciPreKeys)) {
|
||||
warn(TAG, ServiceIdType.ACI, "Prekey consistency check failed! Must rotate keys!")
|
||||
true
|
||||
} else if (!checkPreKeyConsistency(ServiceIdType.PNI, ApplicationDependencies.getProtocolStore().pni(), SignalStore.account().pniPreKeys)) {
|
||||
warn(TAG, ServiceIdType.PNI, "Prekey consistency check failed! Must rotate keys!")
|
||||
warn(TAG, ServiceIdType.PNI, "Prekey consistency check failed! Must rotate keys! (ACI consistency check must have passed)")
|
||||
true
|
||||
} else {
|
||||
warn(TAG, "Forced rotation was requested, but the consistency checks passed!")
|
||||
val timeSinceLastForcedRotation = System.currentTimeMillis() - SignalStore.misc().lastForcedPreKeyRefresh
|
||||
// We check < 0 in case someone changed their clock and had a bad value set
|
||||
timeSinceLastForcedRotation > FeatureFlags.preKeyForceRefreshInterval() || timeSinceLastForcedRotation < 0
|
||||
|
@ -290,10 +295,15 @@ class PreKeysSyncJob private constructor(
|
|||
|
||||
class Factory : Job.Factory<PreKeysSyncJob> {
|
||||
override fun create(parameters: Parameters, serializedData: ByteArray?): PreKeysSyncJob {
|
||||
return serializedData?.let {
|
||||
val data = PreKeysSyncJobData.ADAPTER.decode(serializedData)
|
||||
PreKeysSyncJob(parameters, data.forceRefreshRequested)
|
||||
} ?: PreKeysSyncJob(parameters, forceRotationRequested = false)
|
||||
return try {
|
||||
serializedData?.let {
|
||||
val data = PreKeysSyncJobData.ADAPTER.decode(serializedData)
|
||||
PreKeysSyncJob(parameters, data.forceRefreshRequested)
|
||||
} ?: PreKeysSyncJob(parameters, forceRotationRequested = false)
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, "Error deserializing PreKeysSyncJob", e)
|
||||
PreKeysSyncJob(parameters, forceRotationRequested = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -594,8 +594,13 @@ public abstract class PushSendJob extends SendJob {
|
|||
SignalDatabase.messages().markAsRateLimited(messageId);
|
||||
}
|
||||
|
||||
if (proofRequired.getOptions().contains(ProofRequiredException.Option.RECAPTCHA)) {
|
||||
Log.i(TAG, "[Proof Required] ReCAPTCHA required.");
|
||||
final Optional<ProofRequiredException.Option> captchaRequired =
|
||||
proofRequired.getOptions().stream()
|
||||
.filter(option -> option.equals(ProofRequiredException.Option.RECAPTCHA) || option.equals(ProofRequiredException.Option.CAPTCHA))
|
||||
.findFirst();
|
||||
|
||||
if (captchaRequired.isPresent()) {
|
||||
Log.i(TAG, "[Proof Required] " + captchaRequired.get() + " required.");
|
||||
SignalStore.rateLimit().markNeedsRecaptcha(proofRequired.getToken());
|
||||
|
||||
if (recipient != null) {
|
||||
|
|
|
@ -129,7 +129,7 @@ public class RetrieveProfileAvatarJob extends BaseJob {
|
|||
AvatarHelper.setAvatar(context, recipient.getId(), avatarStream);
|
||||
|
||||
if (recipient.isSelf()) {
|
||||
SignalStore.misc().markHasEverHadAnAvatar();
|
||||
SignalStore.misc().setHasEverHadAnAvatar(true);
|
||||
}
|
||||
} catch (AssertionError e) {
|
||||
throw new IOException("Failed to copy stream. Likely a Conscrypt issue.", e);
|
||||
|
|
|
@ -196,6 +196,11 @@ public class SendViewedReceiptJob extends BaseJob {
|
|||
return;
|
||||
}
|
||||
|
||||
if (recipient.isReleaseNotes()) {
|
||||
Log.w(TAG, "Refusing to send receipts to release channel");
|
||||
return;
|
||||
}
|
||||
|
||||
SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender();
|
||||
SignalServiceAddress remoteAddress = RecipientUtil.toSignalServiceAddress(context, recipient);
|
||||
SignalServiceReceiptMessage receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.VIEWED,
|
||||
|
|
|
@ -411,10 +411,7 @@ public class StorageSyncJob extends BaseJob {
|
|||
new GroupV1RecordProcessor(context).process(records.gv1, StorageSyncHelper.KEY_GENERATOR);
|
||||
new GroupV2RecordProcessor(context).process(records.gv2, StorageSyncHelper.KEY_GENERATOR);
|
||||
new AccountRecordProcessor(context, freshSelf()).process(records.account, StorageSyncHelper.KEY_GENERATOR);
|
||||
|
||||
if (getKnownTypes().contains(ManifestRecord.Identifier.Type.STORY_DISTRIBUTION_LIST.getValue())) {
|
||||
new StoryDistributionListRecordProcessor().process(records.storyDistributionLists, StorageSyncHelper.KEY_GENERATOR);
|
||||
}
|
||||
new StoryDistributionListRecordProcessor().process(records.storyDistributionLists, StorageSyncHelper.KEY_GENERATOR);
|
||||
}
|
||||
|
||||
private static @NonNull List<StorageId> getAllLocalStorageIds(@NonNull Recipient self) {
|
||||
|
|
|
@ -1,372 +0,0 @@
|
|||
package org.thoughtcrime.securesms.keyvalue;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.UsernameQrCodeColorScheme;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.PendingChangeNumberMetadata;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.ChangeNumberConstraintObserver;
|
||||
import org.thoughtcrime.securesms.util.SecurePreferenceManager;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public final class MiscellaneousValues extends SignalStoreValues {
|
||||
|
||||
private static final String LAST_PREKEY_REFRESH_TIME = "last_prekey_refresh_time";
|
||||
private static final String MESSAGE_REQUEST_ENABLE_TIME = "message_request_enable_time";
|
||||
private static final String LAST_PROFILE_REFRESH_TIME = "misc.last_profile_refresh_time";
|
||||
private static final String USERNAME_SHOW_REMINDER = "username.show.reminder";
|
||||
private static final String CLIENT_DEPRECATED = "misc.client_deprecated";
|
||||
private static final String OLD_DEVICE_TRANSFER_LOCKED = "misc.old_device.transfer.locked";
|
||||
private static final String HAS_EVER_HAD_AN_AVATAR = "misc.has.ever.had.an.avatar";
|
||||
private static final String CHANGE_NUMBER_LOCK = "misc.change_number.lock";
|
||||
private static final String PENDING_CHANGE_NUMBER_METADATA = "misc.pending_change_number.metadata";
|
||||
private static final String CENSORSHIP_LAST_CHECK_TIME = "misc.censorship.last_check_time";
|
||||
private static final String CENSORSHIP_SERVICE_REACHABLE = "misc.censorship.service_reachable";
|
||||
private static final String LAST_GV2_PROFILE_CHECK_TIME = "misc.last_gv2_profile_check_time";
|
||||
private static final String CDS_TOKEN = "misc.cds_token";
|
||||
private static final String CDS_BLOCKED_UNTIL = "misc.cds_blocked_until";
|
||||
private static final String LAST_FOREGROUND_TIME = "misc.last_foreground_time";
|
||||
private static final String PNI_INITIALIZED_DEVICES = "misc.pni_initialized_devices";
|
||||
private static final String LINKED_DEVICES_REMINDER = "misc.linked_devices_reminder";
|
||||
private static final String HAS_LINKED_DEVICES = "misc.linked_devices_present";
|
||||
private static final String USERNAME_QR_CODE_COLOR = "mis.username_qr_color_scheme";
|
||||
private static final String KEYBOARD_LANDSCAPE_HEIGHT = "misc.keyboard.landscape_height";
|
||||
private static final String KEYBOARD_PORTRAIT_HEIGHT = "misc.keyboard.protrait_height";
|
||||
private static final String LAST_CONSISTENCY_CHECK_TIME = "misc.last_consistency_check_time";
|
||||
private static final String SERVER_TIME_OFFSET = "misc.server_time_offset";
|
||||
private static final String LAST_SERVER_TIME_OFFSET_UPDATE = "misc.last_server_time_offset_update";
|
||||
private static final String NEEDS_USERNAME_RESTORE = "misc.needs_username_restore";
|
||||
private static final String LAST_FORCED_PREKEY_REFRESH = "misc.last_forced_prekey_refresh";
|
||||
private static final String LAST_CDS_FOREGROUND_SYNC = "misc.last_cds_foreground_sync";
|
||||
|
||||
MiscellaneousValues(@NonNull KeyValueStore store) {
|
||||
super(store);
|
||||
}
|
||||
|
||||
@Override
|
||||
void onFirstEverAppLaunch() {
|
||||
putLong(MESSAGE_REQUEST_ENABLE_TIME, 0);
|
||||
putBoolean(NEEDS_USERNAME_RESTORE, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull List<String> getKeysToIncludeInBackup() {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the last time a _full_ prekey refreshed finished. That means signed+one-time prekeys for both ACI and PNI.
|
||||
*/
|
||||
public long getLastFullPrekeyRefreshTime() {
|
||||
return getLong(LAST_PREKEY_REFRESH_TIME, 0);
|
||||
}
|
||||
|
||||
public void setLastFullPrekeyRefreshTime(long time) {
|
||||
putLong(LAST_PREKEY_REFRESH_TIME, time);
|
||||
}
|
||||
|
||||
public long getMessageRequestEnableTime() {
|
||||
return getLong(MESSAGE_REQUEST_ENABLE_TIME, 0);
|
||||
}
|
||||
|
||||
public long getLastProfileRefreshTime() {
|
||||
return getLong(LAST_PROFILE_REFRESH_TIME, 0);
|
||||
}
|
||||
|
||||
public void setLastProfileRefreshTime(long time) {
|
||||
putLong(LAST_PROFILE_REFRESH_TIME, time);
|
||||
}
|
||||
|
||||
public void hideUsernameReminder() {
|
||||
putBoolean(USERNAME_SHOW_REMINDER, false);
|
||||
}
|
||||
|
||||
public boolean shouldShowUsernameReminder() {
|
||||
return getBoolean(USERNAME_SHOW_REMINDER, true);
|
||||
}
|
||||
|
||||
public boolean isClientDeprecated() {
|
||||
return getBoolean(CLIENT_DEPRECATED, false);
|
||||
}
|
||||
|
||||
public void markClientDeprecated() {
|
||||
putBoolean(CLIENT_DEPRECATED, true);
|
||||
}
|
||||
|
||||
public void clearClientDeprecated() {
|
||||
putBoolean(CLIENT_DEPRECATED, false);
|
||||
}
|
||||
|
||||
public boolean isOldDeviceTransferLocked() {
|
||||
return getBoolean(OLD_DEVICE_TRANSFER_LOCKED, false);
|
||||
}
|
||||
|
||||
public void markOldDeviceTransferLocked() {
|
||||
putBoolean(OLD_DEVICE_TRANSFER_LOCKED, true);
|
||||
}
|
||||
|
||||
public void clearOldDeviceTransferLocked() {
|
||||
putBoolean(OLD_DEVICE_TRANSFER_LOCKED, false);
|
||||
}
|
||||
|
||||
public boolean hasEverHadAnAvatar() {
|
||||
return getBoolean(HAS_EVER_HAD_AN_AVATAR, false);
|
||||
}
|
||||
|
||||
public void markHasEverHadAnAvatar() {
|
||||
putBoolean(HAS_EVER_HAD_AN_AVATAR, true);
|
||||
}
|
||||
|
||||
public boolean isChangeNumberLocked() {
|
||||
return getBoolean(CHANGE_NUMBER_LOCK, false);
|
||||
}
|
||||
|
||||
public void lockChangeNumber() {
|
||||
putBoolean(CHANGE_NUMBER_LOCK, true);
|
||||
ChangeNumberConstraintObserver.INSTANCE.onChange();
|
||||
}
|
||||
|
||||
public void unlockChangeNumber() {
|
||||
putBoolean(CHANGE_NUMBER_LOCK, false);
|
||||
ChangeNumberConstraintObserver.INSTANCE.onChange();
|
||||
}
|
||||
|
||||
public @Nullable PendingChangeNumberMetadata getPendingChangeNumberMetadata() {
|
||||
return getObject(PENDING_CHANGE_NUMBER_METADATA, null, PendingChangeNumberMetadataSerializer.INSTANCE);
|
||||
}
|
||||
|
||||
/** Store pending new PNI data to be applied after successful change number */
|
||||
public void setPendingChangeNumberMetadata(@NonNull PendingChangeNumberMetadata metadata) {
|
||||
putObject(PENDING_CHANGE_NUMBER_METADATA, metadata, PendingChangeNumberMetadataSerializer.INSTANCE);
|
||||
}
|
||||
|
||||
/** Clear pending new PNI data after confirmed successful or failed change number */
|
||||
public void clearPendingChangeNumberMetadata() {
|
||||
remove(PENDING_CHANGE_NUMBER_METADATA);
|
||||
}
|
||||
|
||||
public long getLastCensorshipServiceReachabilityCheckTime() {
|
||||
return getLong(CENSORSHIP_LAST_CHECK_TIME, 0);
|
||||
}
|
||||
|
||||
public void setLastCensorshipServiceReachabilityCheckTime(long value) {
|
||||
putLong(CENSORSHIP_LAST_CHECK_TIME, value);
|
||||
}
|
||||
|
||||
public boolean isServiceReachableWithoutCircumvention() {
|
||||
return getBoolean(CENSORSHIP_SERVICE_REACHABLE, false);
|
||||
}
|
||||
|
||||
public void setServiceReachableWithoutCircumvention(boolean value) {
|
||||
putBoolean(CENSORSHIP_SERVICE_REACHABLE, value);
|
||||
}
|
||||
|
||||
public long getLastGv2ProfileCheckTime() {
|
||||
return getLong(LAST_GV2_PROFILE_CHECK_TIME, 0);
|
||||
}
|
||||
|
||||
public void setLastGv2ProfileCheckTime(long value) {
|
||||
putLong(LAST_GV2_PROFILE_CHECK_TIME, value);
|
||||
}
|
||||
|
||||
public @Nullable byte[] getCdsToken() {
|
||||
return getBlob(CDS_TOKEN, null);
|
||||
}
|
||||
|
||||
public void setCdsToken(@Nullable byte[] token) {
|
||||
getStore().beginWrite()
|
||||
.putBlob(CDS_TOKEN, token)
|
||||
.commit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the time at which we think the next CDS request will succeed. This should be taken from the service response.
|
||||
*/
|
||||
public void setCdsBlockedUtil(long time) {
|
||||
putLong(CDS_BLOCKED_UNTIL, time);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates that a CDS request will never succeed at the current contact count.
|
||||
*/
|
||||
public void markCdsPermanentlyBlocked() {
|
||||
putLong(CDS_BLOCKED_UNTIL, Long.MAX_VALUE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears any rate limiting state related to CDS.
|
||||
*/
|
||||
public void clearCdsBlocked() {
|
||||
setCdsBlockedUtil(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not we expect the next CDS request to succeed.
|
||||
*/
|
||||
public boolean isCdsBlocked() {
|
||||
return getCdsBlockedUtil() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* This represents the next time we think we'll be able to make a successful CDS request. If it is before this time, we expect the request will fail
|
||||
* (assuming the user still has the same number of new E164s).
|
||||
*/
|
||||
public long getCdsBlockedUtil() {
|
||||
return getLong(CDS_BLOCKED_UNTIL, 0);
|
||||
}
|
||||
|
||||
public long getLastForegroundTime() {
|
||||
return getLong(LAST_FOREGROUND_TIME, 0);
|
||||
}
|
||||
|
||||
public void setLastForegroundTime(long time) {
|
||||
putLong(LAST_FOREGROUND_TIME, time);
|
||||
}
|
||||
|
||||
public boolean hasPniInitializedDevices() {
|
||||
return getBoolean(PNI_INITIALIZED_DEVICES, false);
|
||||
}
|
||||
|
||||
public void setPniInitializedDevices(boolean value) {
|
||||
putBoolean(PNI_INITIALIZED_DEVICES, value);
|
||||
}
|
||||
|
||||
public void setHasLinkedDevices(boolean value) {
|
||||
putBoolean(HAS_LINKED_DEVICES, value);
|
||||
}
|
||||
|
||||
public boolean getHasLinkedDevices() {
|
||||
return getBoolean(HAS_LINKED_DEVICES, false);
|
||||
}
|
||||
|
||||
public void setShouldShowLinkedDevicesReminder(boolean value) {
|
||||
putBoolean(LINKED_DEVICES_REMINDER, value);
|
||||
}
|
||||
|
||||
public boolean getShouldShowLinkedDevicesReminder() {
|
||||
return getBoolean(LINKED_DEVICES_REMINDER, false);
|
||||
}
|
||||
|
||||
/** The color the user saved for rendering their shareable username QR code. */
|
||||
public @NonNull UsernameQrCodeColorScheme getUsernameQrCodeColorScheme() {
|
||||
String serialized = getString(USERNAME_QR_CODE_COLOR, null);
|
||||
return UsernameQrCodeColorScheme.deserialize(serialized);
|
||||
}
|
||||
|
||||
public void setUsernameQrCodeColorScheme(@NonNull UsernameQrCodeColorScheme color) {
|
||||
putString(USERNAME_QR_CODE_COLOR, color.serialize());
|
||||
}
|
||||
|
||||
public int getKeyboardLandscapeHeight() {
|
||||
int height = (int) getLong(KEYBOARD_LANDSCAPE_HEIGHT, 0);
|
||||
if (height == 0) {
|
||||
//noinspection deprecation
|
||||
height = SecurePreferenceManager.getSecurePreferences(ApplicationDependencies.getApplication())
|
||||
.getInt("keyboard_height_landscape", 0);
|
||||
|
||||
if (height > 0) {
|
||||
setKeyboardLandscapeHeight(height);
|
||||
}
|
||||
}
|
||||
return height;
|
||||
}
|
||||
|
||||
public void setKeyboardLandscapeHeight(int height) {
|
||||
putLong(KEYBOARD_LANDSCAPE_HEIGHT, height);
|
||||
}
|
||||
|
||||
public int getKeyboardPortraitHeight() {
|
||||
int height = (int) getInteger(KEYBOARD_PORTRAIT_HEIGHT, 0);
|
||||
if (height == 0) {
|
||||
//noinspection deprecation
|
||||
height = SecurePreferenceManager.getSecurePreferences(ApplicationDependencies.getApplication())
|
||||
.getInt("keyboard_height_portrait", 0);
|
||||
|
||||
if (height > 0) {
|
||||
setKeyboardPortraitHeight(height);
|
||||
}
|
||||
}
|
||||
return height;
|
||||
}
|
||||
|
||||
public void setKeyboardPortraitHeight(int height) {
|
||||
putInteger(KEYBOARD_PORTRAIT_HEIGHT, height);
|
||||
}
|
||||
|
||||
public long getLastConsistencyCheckTime() {
|
||||
return getLong(LAST_CONSISTENCY_CHECK_TIME, 0);
|
||||
}
|
||||
|
||||
public void setLastConsistencyCheckTime(long time) {
|
||||
putLong(LAST_CONSISTENCY_CHECK_TIME, time);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the last-known server time.
|
||||
*/
|
||||
public void setLastKnownServerTime(long serverTime, long currentTime) {
|
||||
getStore()
|
||||
.beginWrite()
|
||||
.putLong(SERVER_TIME_OFFSET, currentTime - serverTime)
|
||||
.putLong(LAST_SERVER_TIME_OFFSET_UPDATE, System.currentTimeMillis())
|
||||
.apply();
|
||||
}
|
||||
|
||||
/**
|
||||
* The last-known offset between our local clock and the server. To get an estimate of the server time, take your current time and subtract this offset. e.g.
|
||||
*
|
||||
* estimatedServerTime = System.currentTimeMillis() - SignalStore.misc().getLastKnownServerTimeOffset()
|
||||
*/
|
||||
public long getLastKnownServerTimeOffset() {
|
||||
return getLong(SERVER_TIME_OFFSET, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* The last time (using our local clock) we updated the server time offset returned by {@link #getLastKnownServerTimeOffset()}}.
|
||||
*/
|
||||
public long getLastKnownServerTimeOffsetUpdateTime() {
|
||||
return getLong(LAST_SERVER_TIME_OFFSET_UPDATE, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not we should attempt to restore the user's username and link.
|
||||
*/
|
||||
public boolean needsUsernameRestore() {
|
||||
return getBoolean(NEEDS_USERNAME_RESTORE, false);
|
||||
}
|
||||
|
||||
public void setNeedsUsernameRestore(boolean value) {
|
||||
putBoolean(NEEDS_USERNAME_RESTORE, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the last time we successfully completed a forced prekey refresh.
|
||||
*/
|
||||
public void setLastForcedPreKeyRefresh(long time) {
|
||||
putLong(LAST_FORCED_PREKEY_REFRESH, time);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the last time we successfully completed a forced prekey refresh.
|
||||
*/
|
||||
public long getLastForcedPreKeyRefresh() {
|
||||
return getLong(LAST_FORCED_PREKEY_REFRESH, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* How long it's been since the last foreground CDS sync, which we do in response to new threads being created.
|
||||
*/
|
||||
public long getLastCdsForegroundSyncTime() {
|
||||
return getLong(LAST_CDS_FOREGROUND_SYNC, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the last time we did a foreground CDS sync.
|
||||
*/
|
||||
public void setLastCdsForegroundSyncTime(long time) {
|
||||
putLong(LAST_CDS_FOREGROUND_SYNC, time);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,239 @@
|
|||
package org.thoughtcrime.securesms.keyvalue
|
||||
|
||||
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.UsernameQrCodeColorScheme
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.PendingChangeNumberMetadata
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.ChangeNumberConstraintObserver
|
||||
import org.thoughtcrime.securesms.keyvalue.protos.LeastActiveLinkedDevice
|
||||
|
||||
internal class MiscellaneousValues internal constructor(store: KeyValueStore) : SignalStoreValues(store) {
|
||||
companion object {
|
||||
private const val LAST_PREKEY_REFRESH_TIME = "last_prekey_refresh_time"
|
||||
private const val MESSAGE_REQUEST_ENABLE_TIME = "message_request_enable_time"
|
||||
private const val LAST_PROFILE_REFRESH_TIME = "misc.last_profile_refresh_time"
|
||||
private const val CLIENT_DEPRECATED = "misc.client_deprecated"
|
||||
private const val OLD_DEVICE_TRANSFER_LOCKED = "misc.old_device.transfer.locked"
|
||||
private const val HAS_EVER_HAD_AN_AVATAR = "misc.has.ever.had.an.avatar"
|
||||
private const val CHANGE_NUMBER_LOCK = "misc.change_number.lock"
|
||||
private const val PENDING_CHANGE_NUMBER_METADATA = "misc.pending_change_number.metadata"
|
||||
private const val CENSORSHIP_LAST_CHECK_TIME = "misc.censorship.last_check_time"
|
||||
private const val CENSORSHIP_SERVICE_REACHABLE = "misc.censorship.service_reachable"
|
||||
private const val LAST_GV2_PROFILE_CHECK_TIME = "misc.last_gv2_profile_check_time"
|
||||
private const val CDS_TOKEN = "misc.cds_token"
|
||||
private const val CDS_BLOCKED_UNTIL = "misc.cds_blocked_until"
|
||||
private const val LAST_FOREGROUND_TIME = "misc.last_foreground_time"
|
||||
private const val PNI_INITIALIZED_DEVICES = "misc.pni_initialized_devices"
|
||||
private const val LINKED_DEVICES_REMINDER = "misc.linked_devices_reminder"
|
||||
private const val HAS_LINKED_DEVICES = "misc.linked_devices_present"
|
||||
private const val USERNAME_QR_CODE_COLOR = "mis.username_qr_color_scheme"
|
||||
private const val KEYBOARD_LANDSCAPE_HEIGHT = "misc.keyboard.landscape_height"
|
||||
private const val KEYBOARD_PORTRAIT_HEIGHT = "misc.keyboard.protrait_height"
|
||||
private const val LAST_CONSISTENCY_CHECK_TIME = "misc.last_consistency_check_time"
|
||||
private const val SERVER_TIME_OFFSET = "misc.server_time_offset"
|
||||
private const val LAST_SERVER_TIME_OFFSET_UPDATE = "misc.last_server_time_offset_update"
|
||||
private const val NEEDS_USERNAME_RESTORE = "misc.needs_username_restore"
|
||||
private const val LAST_FORCED_PREKEY_REFRESH = "misc.last_forced_prekey_refresh"
|
||||
private const val LAST_CDS_FOREGROUND_SYNC = "misc.last_cds_foreground_sync"
|
||||
private const val LINKED_DEVICE_LAST_ACTIVE_CHECK_TIME = "misc.linked_device.last_active_check_time"
|
||||
private const val LEAST_ACTIVE_LINKED_DEVICE = "misc.linked_device.least_active"
|
||||
}
|
||||
|
||||
public override fun onFirstEverAppLaunch() {
|
||||
putLong(MESSAGE_REQUEST_ENABLE_TIME, 0)
|
||||
putBoolean(NEEDS_USERNAME_RESTORE, true)
|
||||
}
|
||||
|
||||
public override fun getKeysToIncludeInBackup(): List<String> {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the last time a _full_ prekey refreshed finished. That means signed+one-time prekeys for both ACI and PNI.
|
||||
*/
|
||||
var lastFullPrekeyRefreshTime by longValue(LAST_PREKEY_REFRESH_TIME, 0)
|
||||
|
||||
val messageRequestEnableTime by longValue(MESSAGE_REQUEST_ENABLE_TIME, 0)
|
||||
|
||||
/**
|
||||
* Get the last time we successfully completed a forced prekey refresh.
|
||||
*/
|
||||
var lastForcedPreKeyRefresh by longValue(LAST_FORCED_PREKEY_REFRESH, 0)
|
||||
|
||||
/**
|
||||
* The last time we completed a routine profile refresh.
|
||||
*/
|
||||
var lastProfileRefreshTime by longValue(LAST_PROFILE_REFRESH_TIME, 0)
|
||||
|
||||
/**
|
||||
* Whether or not the client is currently in a 'deprecated' state, disallowing network access.
|
||||
*/
|
||||
var isClientDeprecated: Boolean by booleanValue(CLIENT_DEPRECATED, false)
|
||||
|
||||
/**
|
||||
* Whether or not we've locked the device after they've transferred to a new one.
|
||||
*/
|
||||
var isOldDeviceTransferLocked by booleanValue(OLD_DEVICE_TRANSFER_LOCKED, false)
|
||||
|
||||
/**
|
||||
* Whether or not the user has ever had an avatar.
|
||||
*/
|
||||
var hasEverHadAnAvatar by booleanValue(HAS_EVER_HAD_AN_AVATAR, false)
|
||||
|
||||
val isChangeNumberLocked: Boolean by booleanValue(CHANGE_NUMBER_LOCK, false)
|
||||
|
||||
fun lockChangeNumber() {
|
||||
putBoolean(CHANGE_NUMBER_LOCK, true)
|
||||
ChangeNumberConstraintObserver.onChange()
|
||||
}
|
||||
|
||||
fun unlockChangeNumber() {
|
||||
putBoolean(CHANGE_NUMBER_LOCK, false)
|
||||
ChangeNumberConstraintObserver.onChange()
|
||||
}
|
||||
|
||||
val pendingChangeNumberMetadata: PendingChangeNumberMetadata?
|
||||
get() = getObject(PENDING_CHANGE_NUMBER_METADATA, null, PendingChangeNumberMetadataSerializer)
|
||||
|
||||
/** Store pending new PNI data to be applied after successful change number */
|
||||
fun setPendingChangeNumberMetadata(metadata: PendingChangeNumberMetadata) {
|
||||
putObject(PENDING_CHANGE_NUMBER_METADATA, metadata, PendingChangeNumberMetadataSerializer)
|
||||
}
|
||||
|
||||
/** Clear pending new PNI data after confirmed successful or failed change number */
|
||||
fun clearPendingChangeNumberMetadata() {
|
||||
remove(PENDING_CHANGE_NUMBER_METADATA)
|
||||
}
|
||||
|
||||
/**
|
||||
* The last time we checked if the service was reachable without censorship circumvention.
|
||||
*/
|
||||
var lastCensorshipServiceReachabilityCheckTime by longValue(CENSORSHIP_LAST_CHECK_TIME, 0)
|
||||
|
||||
/**
|
||||
* Whether or not the service is reachable without censorship circumvention.
|
||||
*/
|
||||
var isServiceReachableWithoutCircumvention by booleanValue(CENSORSHIP_SERVICE_REACHABLE, false)
|
||||
|
||||
/**
|
||||
* The last time we did a routing check to see if our GV2 groups have the latest version of our profile key.
|
||||
*/
|
||||
var lastGv2ProfileCheckTime by longValue(LAST_GV2_PROFILE_CHECK_TIME, 0)
|
||||
|
||||
/**
|
||||
* The CDS token that is used for rate-limiting.
|
||||
*/
|
||||
var cdsToken by nullableBlobValue(CDS_TOKEN, null)
|
||||
|
||||
/**
|
||||
* Indicates that a CDS request will never succeed at the current contact count.
|
||||
*/
|
||||
fun markCdsPermanentlyBlocked() {
|
||||
putLong(CDS_BLOCKED_UNTIL, Long.MAX_VALUE)
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears any rate limiting state related to CDS.
|
||||
*/
|
||||
fun clearCdsBlocked() {
|
||||
cdsBlockedUtil = 0
|
||||
}
|
||||
|
||||
/** Whether or not we expect the next CDS request to succeed.*/
|
||||
val isCdsBlocked: Boolean
|
||||
get() = cdsBlockedUtil > 0
|
||||
|
||||
/**
|
||||
* This represents the next time we think we'll be able to make a successful CDS request. If it is before this time, we expect the request will fail
|
||||
* (assuming the user still has the same number of new E164s).
|
||||
*/
|
||||
var cdsBlockedUtil by longValue(CDS_BLOCKED_UNTIL, 0)
|
||||
|
||||
/**
|
||||
* The last time the user foregrounded the app.
|
||||
*/
|
||||
var lastForegroundTime by longValue(LAST_FOREGROUND_TIME, 0)
|
||||
|
||||
/**
|
||||
* Whether or not we've done the initial "PNP Hello World" dance.
|
||||
*/
|
||||
var hasPniInitializedDevices by booleanValue(PNI_INITIALIZED_DEVICES, false)
|
||||
|
||||
/**
|
||||
* Whether or not the user has linked devices.
|
||||
*/
|
||||
var hasLinkedDevices by booleanValue(HAS_LINKED_DEVICES, false)
|
||||
|
||||
/**
|
||||
* Whether or not we should show a reminder for the user to relink their devices after re-registering.
|
||||
*/
|
||||
var shouldShowLinkedDevicesReminder by booleanValue(LINKED_DEVICES_REMINDER, false)
|
||||
|
||||
/**
|
||||
* The color the user saved for rendering their shareable username QR code.
|
||||
*/
|
||||
var usernameQrCodeColorScheme: UsernameQrCodeColorScheme
|
||||
get() {
|
||||
val serialized = getString(USERNAME_QR_CODE_COLOR, null)
|
||||
return UsernameQrCodeColorScheme.deserialize(serialized)
|
||||
}
|
||||
set(color) {
|
||||
putString(USERNAME_QR_CODE_COLOR, color.serialize())
|
||||
}
|
||||
|
||||
/**
|
||||
* Cached landscape keyboard height.
|
||||
*/
|
||||
var keyboardLandscapeHeight by integerValue(KEYBOARD_LANDSCAPE_HEIGHT, 0)
|
||||
|
||||
/**
|
||||
* Cached portrait keyboard height.
|
||||
*/
|
||||
var keyboardPortraitHeight by integerValue(KEYBOARD_PORTRAIT_HEIGHT, 0)
|
||||
|
||||
/**
|
||||
* The last time we ran an account consistency check via [org.thoughtcrime.securesms.jobs.AccountConsistencyWorkerJob]
|
||||
*/
|
||||
var lastConsistencyCheckTime by longValue(LAST_CONSISTENCY_CHECK_TIME, 0)
|
||||
|
||||
/**
|
||||
* The last-known offset between our local clock and the server. To get an estimate of the server time, take your current time and subtract this offset. e.g.
|
||||
*
|
||||
* estimatedServerTime = System.currentTimeMillis() - SignalStore.misc().getLastKnownServerTimeOffset()
|
||||
*/
|
||||
val lastKnownServerTimeOffset by longValue(SERVER_TIME_OFFSET, 0)
|
||||
|
||||
/**
|
||||
* The last time (using our local clock) we updated the server time offset returned by [.getLastKnownServerTimeOffset]}.
|
||||
*/
|
||||
val lastKnownServerTimeOffsetUpdateTime by longValue(LAST_SERVER_TIME_OFFSET_UPDATE, 0)
|
||||
|
||||
/**
|
||||
* Sets the last-known server time.
|
||||
*/
|
||||
fun setLastKnownServerTime(serverTime: Long, currentTime: Long) {
|
||||
store
|
||||
.beginWrite()
|
||||
.putLong(SERVER_TIME_OFFSET, currentTime - serverTime)
|
||||
.putLong(LAST_SERVER_TIME_OFFSET_UPDATE, System.currentTimeMillis())
|
||||
.apply()
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not we should attempt to restore the user's username and link.
|
||||
*/
|
||||
var needsUsernameRestore by booleanValue(NEEDS_USERNAME_RESTORE, false)
|
||||
|
||||
/**
|
||||
* How long it's been since the last foreground CDS sync, which we do in response to new threads being created.
|
||||
*/
|
||||
var lastCdsForegroundSyncTime by longValue(LAST_CDS_FOREGROUND_SYNC, 0)
|
||||
|
||||
/**
|
||||
* The last time we checked for linked device activity.
|
||||
*/
|
||||
var linkedDeviceLastActiveCheckTime by longValue(LINKED_DEVICE_LAST_ACTIVE_CHECK_TIME, 0)
|
||||
|
||||
/**
|
||||
* Details about the least-active linked device.
|
||||
*/
|
||||
var leastActiveLinkedDevice: LeastActiveLinkedDevice? by protoValue(LEAST_ACTIVE_LINKED_DEVICE, LeastActiveLinkedDevice.ADAPTER)
|
||||
}
|
|
@ -28,6 +28,10 @@ internal fun SignalStoreValues.blobValue(key: String, default: ByteArray): Signa
|
|||
return BlobValue(key, default, this.store)
|
||||
}
|
||||
|
||||
internal fun SignalStoreValues.nullableBlobValue(key: String, default: ByteArray?): SignalStoreValueDelegate<ByteArray?> {
|
||||
return NullableBlobValue(key, default, this.store)
|
||||
}
|
||||
|
||||
internal fun <T : Any?> SignalStoreValues.enumValue(key: String, default: T, serializer: LongSerializer<T>): SignalStoreValueDelegate<T> {
|
||||
return KeyValueEnumValue(key, default, serializer, this.store)
|
||||
}
|
||||
|
@ -114,6 +118,16 @@ private class BlobValue(private val key: String, private val default: ByteArray,
|
|||
}
|
||||
}
|
||||
|
||||
private class NullableBlobValue(private val key: String, private val default: ByteArray?, store: KeyValueStore) : SignalStoreValueDelegate<ByteArray?>(store) {
|
||||
override fun getValue(values: KeyValueStore): ByteArray? {
|
||||
return values.getBlob(key, default)
|
||||
}
|
||||
|
||||
override fun setValue(values: KeyValueStore, value: ByteArray?) {
|
||||
values.beginWrite().putBlob(key, value).apply()
|
||||
}
|
||||
}
|
||||
|
||||
private class KeyValueProtoValue<M>(
|
||||
private val key: String,
|
||||
private val adapter: ProtoAdapter<M>,
|
||||
|
|
|
@ -90,6 +90,7 @@ public class LogSectionSystemInfo implements LogSection {
|
|||
builder.append("Server Time Offset: ").append(locked ? "Unknown" : getLastKnownServerTimeOffset()).append("\n");
|
||||
builder.append("Telecom : ").append(locked ? "Unknown" : AndroidTelecomUtil.getTelecomSupported()).append("\n");
|
||||
builder.append("User-Agent : ").append(StandardUserAgentInterceptor.USER_AGENT).append("\n");
|
||||
builder.append("APNG Animation : ").append(locked ? "Unknown" : DeviceProperties.shouldAllowApngStickerAnimation(context)).append("\n");
|
||||
if (BuildConfig.MANAGES_MOLLY_UPDATES) {
|
||||
builder.append("Update Check URL : ").append(BuildConfig.FDROID_UPDATE_URL).append("\n");
|
||||
}
|
||||
|
|
|
@ -28,12 +28,9 @@ import android.widget.ImageView;
|
|||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.camera.core.CameraSelector;
|
||||
import androidx.camera.core.ImageAnalysis;
|
||||
import androidx.camera.core.ImageCapture;
|
||||
import androidx.camera.core.ImageCaptureException;
|
||||
import androidx.camera.core.ImageProxy;
|
||||
import androidx.camera.core.resolutionselector.ResolutionSelector;
|
||||
import androidx.camera.core.resolutionselector.ResolutionStrategy;
|
||||
import androidx.camera.view.PreviewView;
|
||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||
import androidx.constraintlayout.widget.ConstraintSet;
|
||||
|
@ -50,14 +47,17 @@ import org.thoughtcrime.securesms.LoggingFragment;
|
|||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.animation.AnimationCompleteListener;
|
||||
import org.thoughtcrime.securesms.components.TooltipPopup;
|
||||
import org.thoughtcrime.securesms.mediasend.camerax.CameraXController;
|
||||
import org.thoughtcrime.securesms.mediasend.camerax.CameraXFlashToggleView;
|
||||
import org.thoughtcrime.securesms.mediasend.camerax.CameraXModePolicy;
|
||||
import org.thoughtcrime.securesms.mediasend.camerax.CameraXUtil;
|
||||
import org.thoughtcrime.securesms.mediasend.camerax.PlatformCameraController;
|
||||
import org.thoughtcrime.securesms.mediasend.camerax.SignalCameraController;
|
||||
import org.thoughtcrime.securesms.mediasend.v2.MediaAnimations;
|
||||
import org.thoughtcrime.securesms.mediasend.v2.MediaCountIndicatorButton;
|
||||
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
|
||||
import org.thoughtcrime.securesms.mms.MediaConstraints;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.MemoryFileDescriptor;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
@ -70,6 +70,7 @@ import java.util.concurrent.Executors;
|
|||
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
import kotlin.Unit;
|
||||
|
||||
/**
|
||||
* Camera captured implemented using the CameraX SDK, which uses Camera2 under the hood. Should be
|
||||
|
@ -86,11 +87,12 @@ public class CameraXFragment extends LoggingFragment implements CameraFragment {
|
|||
private static final PreviewView.ScaleType PREVIEW_SCALE_TYPE = PreviewView.ScaleType.FILL_CENTER;
|
||||
|
||||
private PreviewView previewView;
|
||||
private MaterialCardView cameraParent;
|
||||
private ViewGroup controlsContainer;
|
||||
private Controller controller;
|
||||
private View selfieFlash;
|
||||
private MemoryFileDescriptor videoFileDescriptor;
|
||||
private SignalCameraController cameraController;
|
||||
private CameraXController cameraController;
|
||||
private CameraXOrientationListener orientationListener;
|
||||
private Disposable mostRecentItemDisposable = Disposable.disposed();
|
||||
private CameraXModePolicy cameraXModePolicy;
|
||||
|
@ -121,6 +123,7 @@ public class CameraXFragment extends LoggingFragment implements CameraFragment {
|
|||
|
||||
return fragment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(@NonNull Context context) {
|
||||
super.onAttach(context);
|
||||
|
@ -146,7 +149,7 @@ public class CameraXFragment extends LoggingFragment implements CameraFragment {
|
|||
@SuppressLint("MissingPermission")
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
ViewGroup cameraParent = view.findViewById(R.id.camerax_camera_parent);
|
||||
this.cameraParent = view.findViewById(R.id.camerax_camera_parent);
|
||||
|
||||
this.previewView = view.findViewById(R.id.camerax_camera);
|
||||
this.controlsContainer = view.findViewById(R.id.camerax_controls_container);
|
||||
|
@ -156,9 +159,18 @@ public class CameraXFragment extends LoggingFragment implements CameraFragment {
|
|||
|
||||
Log.d(TAG, "Starting CameraX with mode policy " + cameraXModePolicy.getClass().getSimpleName());
|
||||
|
||||
View focusIndicator = view.findViewById(R.id.camerax_focus_indicator);
|
||||
|
||||
cameraController = new SignalCameraController(requireContext(), getViewLifecycleOwner(), previewView, focusIndicator);
|
||||
previewView.setScaleType(PREVIEW_SCALE_TYPE);
|
||||
if (FeatureFlags.customCameraXController()) {
|
||||
View focusIndicator = view.findViewById(R.id.camerax_focus_indicator);
|
||||
cameraController = new SignalCameraController(requireContext(), getViewLifecycleOwner(), previewView, focusIndicator);
|
||||
} else {
|
||||
PlatformCameraController platformController = new PlatformCameraController(requireContext());
|
||||
platformController.initializeAndBind(requireContext(), getViewLifecycleOwner());
|
||||
previewView.setController(platformController.getDelegate());
|
||||
cameraController = platformController;
|
||||
}
|
||||
|
||||
cameraXModePolicy.initialize(cameraController);
|
||||
|
||||
cameraScreenBrightnessController = new CameraScreenBrightnessController(
|
||||
|
@ -169,15 +181,13 @@ public class CameraXFragment extends LoggingFragment implements CameraFragment {
|
|||
previewView.setScaleType(PREVIEW_SCALE_TYPE);
|
||||
|
||||
onOrientationChanged();
|
||||
cameraController.bindToLifecycle(() -> Log.d(TAG, "Camera init complete from onViewCreated"));
|
||||
|
||||
if (FeatureFlags.customCameraXController()) {
|
||||
cameraController.initializeAndBind(requireContext(), getViewLifecycleOwner());
|
||||
}
|
||||
|
||||
if (requireArguments().getBoolean(IS_QR_SCAN_ENABLED, false)) {
|
||||
ImageAnalysis imageAnalysis = new ImageAnalysis.Builder()
|
||||
.setResolutionSelector(new ResolutionSelector.Builder().setResolutionStrategy(ResolutionStrategy.HIGHEST_AVAILABLE_STRATEGY).build())
|
||||
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
|
||||
.build();
|
||||
|
||||
imageAnalysis.setAnalyzer(qrAnalysisExecutor, imageProxy -> {
|
||||
cameraController.setImageAnalysisAnalyzer(qrAnalysisExecutor, imageProxy -> {
|
||||
try {
|
||||
String data = qrProcessor.getScannedData(imageProxy);
|
||||
if (data != null) {
|
||||
|
@ -187,27 +197,7 @@ public class CameraXFragment extends LoggingFragment implements CameraFragment {
|
|||
imageProxy.close();
|
||||
}
|
||||
});
|
||||
|
||||
cameraController.addUseCase(imageAnalysis);
|
||||
}
|
||||
|
||||
view.addOnLayoutChangeListener((v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
|
||||
// Let's assume portrait for now, so 9:16
|
||||
float aspectRatio = CameraFragment.getAspectRatioForOrientation(Configuration.ORIENTATION_PORTRAIT);
|
||||
float width = right - left;
|
||||
float height = Math.min((1f / aspectRatio) * width, bottom - top);
|
||||
|
||||
ViewGroup.LayoutParams params = cameraParent.getLayoutParams();
|
||||
|
||||
// If there's a mismatch...
|
||||
if (params.height != (int) height) {
|
||||
params.width = (int) width;
|
||||
params.height = (int) height;
|
||||
|
||||
cameraParent.setLayoutParams(params);
|
||||
cameraController.setPreviewTargetSize(new Size((int) width, (int) height));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -226,16 +216,10 @@ public class CameraXFragment extends LoggingFragment implements CameraFragment {
|
|||
public void onResume() {
|
||||
super.onResume();
|
||||
|
||||
cameraController.bindToLifecycle(() -> Log.d(TAG, "Camera init complete from onResume"));
|
||||
|
||||
cameraController.bindToLifecycle(getViewLifecycleOwner(), () -> Log.d(TAG, "Camera init complete from onResume"));
|
||||
requireActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
|
@ -278,8 +262,8 @@ public class CameraXFragment extends LoggingFragment implements CameraFragment {
|
|||
private void onOrientationChanged() {
|
||||
int layout = R.layout.camera_controls_portrait;
|
||||
|
||||
int resolution = CameraXUtil.getIdealResolution(Resources.getSystem().getDisplayMetrics().widthPixels, Resources.getSystem().getDisplayMetrics().heightPixels);
|
||||
Size size = CameraXUtil.buildResolutionForRatio(resolution, ASPECT_RATIO_16_9, true);
|
||||
int resolution = CameraXUtil.getIdealResolution(Resources.getSystem().getDisplayMetrics().widthPixels, Resources.getSystem().getDisplayMetrics().heightPixels);
|
||||
Size size = CameraXUtil.buildResolutionForRatio(resolution, ASPECT_RATIO_16_9, true);
|
||||
|
||||
cameraController.setImageCaptureTargetSize(size);
|
||||
|
||||
|
@ -372,15 +356,19 @@ public class CameraXFragment extends LoggingFragment implements CameraFragment {
|
|||
selfieFlash = requireView().findViewById(R.id.camera_selfie_flash);
|
||||
|
||||
captureButton.setOnClickListener(v -> {
|
||||
captureButton.setEnabled(false);
|
||||
flipButton.setEnabled(false);
|
||||
flashButton.setEnabled(false);
|
||||
onCaptureClicked();
|
||||
if (cameraController.isInitialized()) {
|
||||
captureButton.setEnabled(false);
|
||||
flipButton.setEnabled(false);
|
||||
flashButton.setEnabled(false);
|
||||
onCaptureClicked();
|
||||
} else {
|
||||
Log.i(TAG, "Camera capture button clicked but the camera controller is not yet initialized.");
|
||||
}
|
||||
});
|
||||
|
||||
previewView.setScaleType(PREVIEW_SCALE_TYPE);
|
||||
|
||||
cameraController.addInitializationCompletedListener(cameraProvider -> initializeFlipButton(flipButton, flashButton));
|
||||
cameraController.addInitializationCompletedListener(ContextCompat.getMainExecutor(requireContext()), () -> initializeFlipButton(flipButton, flashButton));
|
||||
|
||||
flashButton.setAutoFlashEnabled(cameraController.getImageCaptureFlashMode() >= ImageCapture.FLASH_MODE_AUTO);
|
||||
flashButton.setFlash(cameraController.getImageCaptureFlashMode());
|
||||
|
@ -567,10 +555,10 @@ public class CameraXFragment extends LoggingFragment implements CameraFragment {
|
|||
}
|
||||
|
||||
@SuppressLint({ "MissingPermission" })
|
||||
private void initializeFlipButton(@NonNull View flipButton, @NonNull CameraXFlashToggleView flashButton) {
|
||||
private Unit initializeFlipButton(@NonNull View flipButton, @NonNull CameraXFlashToggleView flashButton) {
|
||||
if (getContext() == null) {
|
||||
Log.w(TAG, "initializeFlipButton called either before or after fragment was attached.");
|
||||
return;
|
||||
return Unit.INSTANCE;
|
||||
}
|
||||
|
||||
getViewLifecycleOwner().getLifecycle().addObserver(cameraScreenBrightnessController);
|
||||
|
@ -607,13 +595,14 @@ public class CameraXFragment extends LoggingFragment implements CameraFragment {
|
|||
} else {
|
||||
flipButton.setVisibility(View.GONE);
|
||||
}
|
||||
return Unit.INSTANCE;
|
||||
}
|
||||
|
||||
private static class CameraStateProvider implements CameraScreenBrightnessController.CameraStateProvider {
|
||||
|
||||
private final SignalCameraController cameraController;
|
||||
private final CameraXController cameraController;
|
||||
|
||||
private CameraStateProvider(SignalCameraController cameraController) {
|
||||
private CameraStateProvider(CameraXController cameraController) {
|
||||
this.cameraController = cameraController;
|
||||
}
|
||||
|
||||
|
@ -637,7 +626,9 @@ public class CameraXFragment extends LoggingFragment implements CameraFragment {
|
|||
@Override
|
||||
public void onOrientationChanged(int orientation) {
|
||||
if (cameraController != null) {
|
||||
cameraController.setImageRotation(orientation);
|
||||
if (FeatureFlags.customCameraXController()) {
|
||||
cameraController.setImageRotation(orientation);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,23 +8,24 @@ import androidx.annotation.NonNull;
|
|||
import androidx.camera.core.CameraSelector;
|
||||
import androidx.camera.core.ImageCapture;
|
||||
|
||||
import org.thoughtcrime.securesms.mediasend.camerax.CameraXController;
|
||||
import org.thoughtcrime.securesms.mediasend.camerax.SignalCameraController;
|
||||
|
||||
final class CameraXSelfieFlashHelper {
|
||||
|
||||
private static final float MAX_SCREEN_BRIGHTNESS = 1f;
|
||||
private static final float MAX_SELFIE_FLASH_ALPHA = 0.9f;
|
||||
private static final float MAX_SCREEN_BRIGHTNESS = 1f;
|
||||
private static final float MAX_SELFIE_FLASH_ALPHA = 0.9f;
|
||||
|
||||
private final Window window;
|
||||
private final SignalCameraController camera;
|
||||
private final View selfieFlash;
|
||||
private final Window window;
|
||||
private final CameraXController camera;
|
||||
private final View selfieFlash;
|
||||
|
||||
private float brightnessBeforeFlash;
|
||||
private boolean inFlash;
|
||||
private int flashMode = -1;
|
||||
|
||||
CameraXSelfieFlashHelper(@NonNull Window window,
|
||||
@NonNull SignalCameraController camera,
|
||||
@NonNull CameraXController camera,
|
||||
@NonNull View selfieFlash)
|
||||
{
|
||||
this.window = window;
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue