Compare commits
103 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f089aa41cf | ||
|
|
d9930cc307 | ||
|
|
576948f5ac | ||
|
|
ed4c298c33 | ||
|
|
0fab27fee9 | ||
|
|
77c73dd493 | ||
|
|
c7a8f58f29 | ||
|
|
9a12e84ad4 | ||
|
|
e500dd0283 | ||
|
|
aa9ba9c668 | ||
|
|
5553ef6a99 | ||
|
|
141a128429 | ||
|
|
52750e726a | ||
|
|
97897a84aa | ||
|
|
78ad67baad | ||
|
|
13aafbfefd | ||
|
|
22dddeb3b7 | ||
|
|
5c64a91864 | ||
|
|
66b6c1656c | ||
|
|
44d1c8c4bb | ||
|
|
599d55ac0b | ||
|
|
9cbe204141 | ||
|
|
8615dfc463 | ||
|
|
72050dbe64 | ||
|
|
9dac02fa1c | ||
|
|
7fdaf6f706 | ||
|
|
4316136723 | ||
|
|
f375750cf8 | ||
|
|
c55f281213 | ||
|
|
64496d1d92 | ||
|
|
8eac4d3a57 | ||
|
|
7b5f7cd808 | ||
|
|
5b99c6681c | ||
|
|
f989ad4014 | ||
|
|
ce5c023f3b | ||
|
|
f22567e4fb | ||
|
|
cdb73d4b8a | ||
|
|
48901f64c7 | ||
|
|
ab4a38d565 | ||
|
|
4e077bbb52 | ||
|
|
4f17aa2b17 | ||
|
|
e7808eb842 | ||
|
|
fe0f7ee5e7 | ||
|
|
c4846d92da | ||
|
|
83cb48d119 | ||
|
|
987f92245d | ||
|
|
aecd17b2f0 | ||
|
|
a7b4a5d93d | ||
|
|
c8d2a06676 | ||
|
|
42d114e75b | ||
|
|
c2abe2fc33 | ||
|
|
fb1c7c346e | ||
|
|
0b196db4b6 | ||
|
|
bdd1858602 | ||
|
|
915181fbb7 | ||
|
|
5f375dc9a6 | ||
|
|
5929866ae0 | ||
|
|
d706fb0c4b | ||
|
|
f4185d2868 | ||
|
|
9430c27e64 | ||
|
|
b724f2b01a | ||
|
|
1e6d575ec9 | ||
|
|
4c7cf5212e | ||
|
|
33ca1132dc | ||
|
|
a5e11abdc9 | ||
|
|
3924f65cbe | ||
|
|
c500d8ecbd | ||
|
|
cd98fd894d | ||
|
|
45a3c44d0c | ||
|
|
8ddec63e31 | ||
|
|
2d2a871194 | ||
|
|
2ef0032a33 | ||
|
|
570a310e2e | ||
|
|
930a263174 | ||
|
|
7df015ceef | ||
|
|
4c1555bc7b | ||
|
|
e877f43dde | ||
|
|
52dcbb8bc6 | ||
|
|
e0dd576cb1 | ||
|
|
fb746b1ad5 | ||
|
|
ef35efe34e | ||
|
|
6a30caff87 | ||
|
|
cb2816362c | ||
|
|
5f67c9363e | ||
|
|
ba76a8323e | ||
|
|
aa9c7f7d7b | ||
|
|
411a0198b4 | ||
|
|
39679ebfc3 | ||
|
|
933b799266 | ||
|
|
d22a2c0a50 | ||
|
|
3f682be609 | ||
|
|
b16481616a | ||
|
|
d44bef0eda | ||
|
|
f02b8001e4 | ||
|
|
fa258dcef2 | ||
|
|
fc547218d1 | ||
|
|
eea29813fa | ||
|
|
276d71d365 | ||
|
|
539276673a | ||
|
|
d6871f8dc2 | ||
|
|
d93543510f | ||
|
|
69f7ad28ec | ||
|
|
8c2ff2f1c2 |
1
.gitattributes
vendored
1
.gitattributes
vendored
@ -1 +1,2 @@
|
|||||||
*.ai binary
|
*.ai binary
|
||||||
|
**/src/screenshotTest*/reference/**/*.png filter=lfs diff=lfs merge=lfs -text
|
||||||
|
|||||||
9
.github/workflows/android.yml
vendored
9
.github/workflows/android.yml
vendored
@ -16,24 +16,25 @@ jobs:
|
|||||||
runs-on: ubuntu-latest-8-cores
|
runs-on: ubuntu-latest-8-cores
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
# gh api repos/actions/checkout/commits/v6 --jq '.sha'
|
# gh api repos/actions/checkout/commits/v6 --jq '.sha'
|
||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
|
lfs: true
|
||||||
|
|
||||||
- name: set up JDK 17
|
- name: set up JDK 17
|
||||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5
|
uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5
|
||||||
# gh api repos/actions/setup-java/commits/v5 --jq '.sha'
|
# gh api repos/actions/setup-java/commits/v5 --jq '.sha'
|
||||||
with:
|
with:
|
||||||
distribution: temurin
|
distribution: temurin
|
||||||
java-version: 17
|
java-version: 17
|
||||||
|
|
||||||
- name: Validate Gradle Wrapper
|
- name: Validate Gradle Wrapper
|
||||||
uses: gradle/actions/wrapper-validation@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6
|
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # v6
|
||||||
# gh api repos/gradle/actions/commits/v6 --jq '.sha'
|
# gh api repos/gradle/actions/commits/v6 --jq '.sha'
|
||||||
|
|
||||||
- name: Set up Gradle
|
- name: Set up Gradle
|
||||||
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6
|
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6
|
||||||
# gh api repos/gradle/actions/commits/v6 --jq '.sha'
|
# gh api repos/gradle/actions/commits/v6 --jq '.sha'
|
||||||
with:
|
with:
|
||||||
# Only 8.** branch builds write to the cache; everything else (PRs, etc.) reads only.
|
# Only 8.** branch builds write to the cache; everything else (PRs, etc.) reads only.
|
||||||
|
|||||||
10
.github/workflows/diffuse.yml
vendored
10
.github/workflows/diffuse.yml
vendored
@ -16,21 +16,21 @@ jobs:
|
|||||||
runs-on: ubuntu-latest-8-cores
|
runs-on: ubuntu-latest-8-cores
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
# gh api repos/actions/checkout/commits/v6 --jq '.sha'
|
# gh api repos/actions/checkout/commits/v6 --jq '.sha'
|
||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
ref: ${{ github.event.pull_request.base.sha }}
|
ref: ${{ github.event.pull_request.base.sha }}
|
||||||
|
|
||||||
- name: set up JDK 17
|
- name: set up JDK 17
|
||||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5
|
uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5
|
||||||
# gh api repos/actions/setup-java/commits/v5 --jq '.sha'
|
# gh api repos/actions/setup-java/commits/v5 --jq '.sha'
|
||||||
with:
|
with:
|
||||||
distribution: temurin
|
distribution: temurin
|
||||||
java-version: 17
|
java-version: 17
|
||||||
|
|
||||||
- name: Set up Gradle
|
- name: Set up Gradle
|
||||||
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6
|
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6
|
||||||
# gh api repos/gradle/actions/commits/v6 --jq '.sha'
|
# gh api repos/gradle/actions/commits/v6 --jq '.sha'
|
||||||
with:
|
with:
|
||||||
# PR-only workflow: always read from the cache, never write.
|
# PR-only workflow: always read from the cache, never write.
|
||||||
@ -42,7 +42,7 @@ jobs:
|
|||||||
run: echo "y" | ${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager --install "ndk;${{ env.NDK_VERSION }}"
|
run: echo "y" | ${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager --install "ndk;${{ env.NDK_VERSION }}"
|
||||||
|
|
||||||
- name: Validate Gradle Wrapper
|
- name: Validate Gradle Wrapper
|
||||||
uses: gradle/actions/wrapper-validation@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6
|
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # v6
|
||||||
# gh api repos/gradle/actions/commits/v6 --jq '.sha'
|
# gh api repos/gradle/actions/commits/v6 --jq '.sha'
|
||||||
|
|
||||||
- name: Cache base apk
|
- name: Cache base apk
|
||||||
@ -61,7 +61,7 @@ jobs:
|
|||||||
if: steps.cache-base.outputs.cache-hit != 'true'
|
if: steps.cache-base.outputs.cache-hit != 'true'
|
||||||
run: mv app/build/outputs/apk/playProd/release/*arm64*.apk diffuse-base.apk
|
run: mv app/build/outputs/apk/playProd/release/*arm64*.apk diffuse-base.apk
|
||||||
|
|
||||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
# gh api repos/actions/checkout/commits/v6 --jq '.sha'
|
# gh api repos/actions/checkout/commits/v6 --jq '.sha'
|
||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
|
|||||||
2
.github/workflows/docker.yml
vendored
2
.github/workflows/docker.yml
vendored
@ -11,7 +11,7 @@ jobs:
|
|||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
# gh api repos/actions/checkout/commits/v6 --jq '.sha'
|
# gh api repos/actions/checkout/commits/v6 --jq '.sha'
|
||||||
- name: Build image
|
- name: Build image
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@ -16,6 +16,7 @@ plugins {
|
|||||||
alias(libs.plugins.ktlint)
|
alias(libs.plugins.ktlint)
|
||||||
alias(libs.plugins.compose.compiler)
|
alias(libs.plugins.compose.compiler)
|
||||||
alias(libs.plugins.kotlinx.serialization)
|
alias(libs.plugins.kotlinx.serialization)
|
||||||
|
alias(testLibs.plugins.compose.screenshot)
|
||||||
alias(benchmarkLibs.plugins.baselineprofile)
|
alias(benchmarkLibs.plugins.baselineprofile)
|
||||||
id("androidx.navigation.safeargs")
|
id("androidx.navigation.safeargs")
|
||||||
id("kotlin-parcelize")
|
id("kotlin-parcelize")
|
||||||
@ -27,8 +28,8 @@ plugins {
|
|||||||
val staticIps = Properties().apply { file("static-ips.properties").reader().use { load(it) } }
|
val staticIps = Properties().apply { file("static-ips.properties").reader().use { load(it) } }
|
||||||
staticIps.stringPropertyNames().forEach { rootProject.extra[it] = staticIps.getProperty(it) }
|
staticIps.stringPropertyNames().forEach { rootProject.extra[it] = staticIps.getProperty(it) }
|
||||||
|
|
||||||
val canonicalVersionCode = 1706
|
val canonicalVersionCode = 1711
|
||||||
val canonicalVersionName = "8.15.2"
|
val canonicalVersionName = "8.17.1"
|
||||||
val currentHotfixVersion = 0
|
val currentHotfixVersion = 0
|
||||||
val maxHotfixVersions = 100
|
val maxHotfixVersions = 100
|
||||||
|
|
||||||
@ -127,9 +128,16 @@ ktlint {
|
|||||||
version.set("1.5.0")
|
version.set("1.5.0")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
screenshotTests {
|
||||||
|
// Fraction of differing pixels tolerated before a screenshot test fails (0.0001 = 0.01%).
|
||||||
|
imageDifferenceThreshold = 0.0001f
|
||||||
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "org.thoughtcrime.securesms"
|
namespace = "org.thoughtcrime.securesms"
|
||||||
|
|
||||||
|
experimentalProperties["android.experimental.enableScreenshotTest"] = true
|
||||||
|
|
||||||
buildToolsVersion = libs.versions.buildTools.get()
|
buildToolsVersion = libs.versions.buildTools.get()
|
||||||
compileSdkVersion(libs.versions.compileSdk.get())
|
compileSdkVersion(libs.versions.compileSdk.get())
|
||||||
ndkVersion = libs.versions.ndk.get()
|
ndkVersion = libs.versions.ndk.get()
|
||||||
@ -712,6 +720,11 @@ dependencies {
|
|||||||
}
|
}
|
||||||
implementation(libs.lottie)
|
implementation(libs.lottie)
|
||||||
implementation(libs.lottie.compose)
|
implementation(libs.lottie.compose)
|
||||||
|
|
||||||
|
// Compose screenshot testing
|
||||||
|
screenshotTestImplementation(testLibs.compose.screenshot.validation.api)
|
||||||
|
screenshotTestImplementation(libs.androidx.compose.ui.tooling.core)
|
||||||
|
screenshotTestImplementation(libs.androidx.compose.ui.tooling.preview)
|
||||||
implementation(libs.signal.android.database.sqlcipher)
|
implementation(libs.signal.android.database.sqlcipher)
|
||||||
implementation(libs.androidx.sqlite)
|
implementation(libs.androidx.sqlite)
|
||||||
testImplementation(libs.androidx.sqlite.framework)
|
testImplementation(libs.androidx.sqlite.framework)
|
||||||
|
|||||||
37237
app/lint-baseline.xml
37237
app/lint-baseline.xml
File diff suppressed because one or more lines are too long
@ -8,6 +8,11 @@
|
|||||||
-keep class org.thoughtcrime.securesms.** { *; }
|
-keep class org.thoughtcrime.securesms.** { *; }
|
||||||
-keep class org.signal.donations.json.** { *; }
|
-keep class org.signal.donations.json.** { *; }
|
||||||
-keep class org.signal.network.** { *; }
|
-keep class org.signal.network.** { *; }
|
||||||
|
|
||||||
|
-keep class org.signal.core.util.crypto.AttachmentSecret { *; }
|
||||||
|
-keep class org.signal.core.util.crypto.AttachmentSecret$* { *; }
|
||||||
|
-keep class org.signal.core.util.crypto.KeyStoreHelper$SealedData { *; }
|
||||||
|
-keep class org.signal.core.util.crypto.KeyStoreHelper$SealedData$* { *; }
|
||||||
-keepclassmembers class ** {
|
-keepclassmembers class ** {
|
||||||
public void onEvent*(**);
|
public void onEvent*(**);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,9 +9,11 @@ import org.thoughtcrime.securesms.database.LogDatabase
|
|||||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencyProvider
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencyProvider
|
||||||
import org.thoughtcrime.securesms.dependencies.InstrumentationApplicationDependencyProvider
|
import org.thoughtcrime.securesms.dependencies.InstrumentationApplicationDependencyProvider
|
||||||
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
import org.thoughtcrime.securesms.logging.CustomSignalProtocolLogger
|
import org.thoughtcrime.securesms.logging.CustomSignalProtocolLogger
|
||||||
import org.thoughtcrime.securesms.logging.PersistentLogger
|
import org.thoughtcrime.securesms.logging.PersistentLogger
|
||||||
import org.thoughtcrime.securesms.testing.InMemoryLogger
|
import org.thoughtcrime.securesms.testing.InMemoryLogger
|
||||||
|
import org.thoughtcrime.securesms.testing.TestRemoteConfig
|
||||||
import org.thoughtcrime.securesms.util.Environment
|
import org.thoughtcrime.securesms.util.Environment
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -30,6 +32,13 @@ class SignalInstrumentationApplicationContext : ApplicationContext() {
|
|||||||
val default = ApplicationDependencyProvider(this)
|
val default = ApplicationDependencyProvider(this)
|
||||||
AppDependencies.init(this, InstrumentationApplicationDependencyProvider(this, default))
|
AppDependencies.init(this, InstrumentationApplicationDependencyProvider(this, default))
|
||||||
AppDependencies.deadlockDetector.start()
|
AppDependencies.deadlockDetector.start()
|
||||||
|
|
||||||
|
// Stage any test-declared remote config into the store to be read in RemoteConfig.init().
|
||||||
|
if (TestRemoteConfig.pending.isNotEmpty()) {
|
||||||
|
val json = TestRemoteConfig.json
|
||||||
|
SignalStore.remoteConfig.currentConfig = json
|
||||||
|
SignalStore.remoteConfig.pendingConfig = json
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun initializeLogging() {
|
override fun initializeLogging() {
|
||||||
|
|||||||
@ -20,22 +20,22 @@ import org.junit.Rule
|
|||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import org.signal.core.models.backup.MediaName
|
import org.signal.core.models.backup.MediaName
|
||||||
|
import org.signal.core.models.database.AttachmentId
|
||||||
import org.signal.core.models.media.TransformProperties
|
import org.signal.core.models.media.TransformProperties
|
||||||
import org.signal.core.util.Base64
|
import org.signal.core.util.Base64
|
||||||
import org.signal.core.util.Base64.decodeBase64OrThrow
|
import org.signal.core.util.Base64.decodeBase64OrThrow
|
||||||
import org.signal.core.util.copyTo
|
import org.signal.core.util.copyTo
|
||||||
import org.signal.core.util.stream.NullOutputStream
|
import org.signal.core.util.stream.NullOutputStream
|
||||||
|
import org.signal.mediasend.SentMediaQuality
|
||||||
import org.thoughtcrime.securesms.attachments.ArchivedAttachment
|
import org.thoughtcrime.securesms.attachments.ArchivedAttachment
|
||||||
import org.thoughtcrime.securesms.attachments.Attachment
|
import org.thoughtcrime.securesms.attachments.Attachment
|
||||||
import org.thoughtcrime.securesms.attachments.AttachmentId
|
|
||||||
import org.thoughtcrime.securesms.attachments.PointerAttachment
|
import org.thoughtcrime.securesms.attachments.PointerAttachment
|
||||||
import org.thoughtcrime.securesms.attachments.UriAttachment
|
import org.thoughtcrime.securesms.attachments.UriAttachment
|
||||||
import org.thoughtcrime.securesms.backup.v2.ArchivedMediaObject
|
import org.thoughtcrime.securesms.backup.v2.ArchivedMediaObject
|
||||||
|
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
import org.thoughtcrime.securesms.mms.IncomingMessage
|
import org.thoughtcrime.securesms.mms.IncomingMessage
|
||||||
import org.thoughtcrime.securesms.mms.MediaStream
|
import org.thoughtcrime.securesms.mms.MediaStream
|
||||||
import org.thoughtcrime.securesms.mms.SentMediaQuality
|
|
||||||
import org.thoughtcrime.securesms.providers.BlobProvider
|
|
||||||
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||||
import org.thoughtcrime.securesms.util.MediaUtil
|
import org.thoughtcrime.securesms.util.MediaUtil
|
||||||
import org.whispersystems.signalservice.api.crypto.AttachmentCipherOutputStream
|
import org.whispersystems.signalservice.api.crypto.AttachmentCipherOutputStream
|
||||||
@ -67,7 +67,7 @@ class AttachmentTableTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun givenABlob_whenIInsert2AttachmentsForPreUpload_thenIExpectDistinctIdsButSameFileName() {
|
fun givenABlob_whenIInsert2AttachmentsForPreUpload_thenIExpectDistinctIdsButSameFileName() {
|
||||||
val blob = BlobProvider.getInstance().forData(byteArrayOf(1, 2, 3, 4, 5)).createForSingleSessionInMemory()
|
val blob = AppDependencies.blobs.forData(byteArrayOf(1, 2, 3, 4, 5)).createForSingleSessionInMemory()
|
||||||
val highQualityProperties = createHighQualityTransformProperties()
|
val highQualityProperties = createHighQualityTransformProperties()
|
||||||
val highQualityImage = createAttachment(1, blob, highQualityProperties)
|
val highQualityImage = createAttachment(1, blob, highQualityProperties)
|
||||||
val attachment = SignalDatabase.attachments.insertAttachmentForPreUpload(highQualityImage)
|
val attachment = SignalDatabase.attachments.insertAttachmentForPreUpload(highQualityImage)
|
||||||
@ -80,7 +80,7 @@ class AttachmentTableTest {
|
|||||||
@FlakyTest
|
@FlakyTest
|
||||||
@Test
|
@Test
|
||||||
fun givenABlobAndDifferentTransformQuality_whenIInsert2AttachmentsForPreUpload_thenIExpectDifferentFileInfos() {
|
fun givenABlobAndDifferentTransformQuality_whenIInsert2AttachmentsForPreUpload_thenIExpectDifferentFileInfos() {
|
||||||
val blob = BlobProvider.getInstance().forData(byteArrayOf(1, 2, 3, 4, 5)).createForSingleSessionInMemory()
|
val blob = AppDependencies.blobs.forData(byteArrayOf(1, 2, 3, 4, 5)).createForSingleSessionInMemory()
|
||||||
val highQualityProperties = createHighQualityTransformProperties()
|
val highQualityProperties = createHighQualityTransformProperties()
|
||||||
val highQualityImage = createAttachment(1, blob, highQualityProperties)
|
val highQualityImage = createAttachment(1, blob, highQualityProperties)
|
||||||
val lowQualityImage = createAttachment(1, blob, TransformProperties.empty())
|
val lowQualityImage = createAttachment(1, blob, TransformProperties.empty())
|
||||||
@ -107,7 +107,7 @@ class AttachmentTableTest {
|
|||||||
@Ignore("test is flaky")
|
@Ignore("test is flaky")
|
||||||
@Test
|
@Test
|
||||||
fun givenIdenticalAttachmentsInsertedForPreUpload_whenIUpdateAttachmentDataAndSpecifyOnlyModifyThisAttachment_thenIExpectDifferentFileInfos() {
|
fun givenIdenticalAttachmentsInsertedForPreUpload_whenIUpdateAttachmentDataAndSpecifyOnlyModifyThisAttachment_thenIExpectDifferentFileInfos() {
|
||||||
val blob = BlobProvider.getInstance().forData(byteArrayOf(1, 2, 3, 4, 5)).createForSingleSessionInMemory()
|
val blob = AppDependencies.blobs.forData(byteArrayOf(1, 2, 3, 4, 5)).createForSingleSessionInMemory()
|
||||||
val highQualityProperties = createHighQualityTransformProperties()
|
val highQualityProperties = createHighQualityTransformProperties()
|
||||||
val highQualityImage = createAttachment(1, blob, highQualityProperties)
|
val highQualityImage = createAttachment(1, blob, highQualityProperties)
|
||||||
val attachment = SignalDatabase.attachments.insertAttachmentForPreUpload(highQualityImage)
|
val attachment = SignalDatabase.attachments.insertAttachmentForPreUpload(highQualityImage)
|
||||||
@ -143,9 +143,9 @@ class AttachmentTableTest {
|
|||||||
val uncompressData = byteArrayOf(1, 2, 3, 4, 5)
|
val uncompressData = byteArrayOf(1, 2, 3, 4, 5)
|
||||||
val compressedData = byteArrayOf(1, 2, 3)
|
val compressedData = byteArrayOf(1, 2, 3)
|
||||||
|
|
||||||
val blobUncompressed = BlobProvider.getInstance().forData(uncompressData).createForSingleSessionInMemory()
|
val blobUncompressed = AppDependencies.blobs.forData(uncompressData).createForSingleSessionInMemory()
|
||||||
|
|
||||||
val previousAttachment = createAttachment(1, BlobProvider.getInstance().forData(compressedData).createForSingleSessionInMemory(), TransformProperties.empty())
|
val previousAttachment = createAttachment(1, AppDependencies.blobs.forData(compressedData).createForSingleSessionInMemory(), TransformProperties.empty())
|
||||||
val previousDatabaseAttachmentId: AttachmentId = SignalDatabase.attachments.insertAttachmentsForMessage(1, listOf(previousAttachment), emptyList()).values.first()
|
val previousDatabaseAttachmentId: AttachmentId = SignalDatabase.attachments.insertAttachmentsForMessage(1, listOf(previousAttachment), emptyList()).values.first()
|
||||||
|
|
||||||
val standardQualityPreUpload = createAttachment(1, blobUncompressed, TransformProperties.empty())
|
val standardQualityPreUpload = createAttachment(1, blobUncompressed, TransformProperties.empty())
|
||||||
@ -178,7 +178,7 @@ class AttachmentTableTest {
|
|||||||
fun doNotDedupedFileIfUsedByAnotherAttachmentWithADifferentTransformProperties() {
|
fun doNotDedupedFileIfUsedByAnotherAttachmentWithADifferentTransformProperties() {
|
||||||
// GIVEN
|
// GIVEN
|
||||||
val uncompressData = byteArrayOf(1, 2, 3, 4, 5)
|
val uncompressData = byteArrayOf(1, 2, 3, 4, 5)
|
||||||
val blobUncompressed = BlobProvider.getInstance().forData(uncompressData).createForSingleSessionInMemory()
|
val blobUncompressed = AppDependencies.blobs.forData(uncompressData).createForSingleSessionInMemory()
|
||||||
|
|
||||||
val standardQualityPreUpload = createAttachment(1, blobUncompressed, TransformProperties.empty())
|
val standardQualityPreUpload = createAttachment(1, blobUncompressed, TransformProperties.empty())
|
||||||
val standardDatabaseAttachment = SignalDatabase.attachments.insertAttachmentForPreUpload(standardQualityPreUpload)
|
val standardDatabaseAttachment = SignalDatabase.attachments.insertAttachmentForPreUpload(standardQualityPreUpload)
|
||||||
@ -204,7 +204,7 @@ class AttachmentTableTest {
|
|||||||
@Test
|
@Test
|
||||||
fun resetArchiveTransferStateByPlaintextHashAndRemoteKey_singleMatch() {
|
fun resetArchiveTransferStateByPlaintextHashAndRemoteKey_singleMatch() {
|
||||||
// Given an attachment with some plaintextHash+remoteKey
|
// Given an attachment with some plaintextHash+remoteKey
|
||||||
val blob = BlobProvider.getInstance().forData(byteArrayOf(1, 2, 3, 4, 5)).createForSingleSessionInMemory()
|
val blob = AppDependencies.blobs.forData(byteArrayOf(1, 2, 3, 4, 5)).createForSingleSessionInMemory()
|
||||||
val attachment = createAttachment(1, blob, TransformProperties.empty())
|
val attachment = createAttachment(1, blob, TransformProperties.empty())
|
||||||
val attachmentId = SignalDatabase.attachments.insertAttachmentsForMessage(-1L, listOf(attachment), emptyList()).values.first()
|
val attachmentId = SignalDatabase.attachments.insertAttachmentsForMessage(-1L, listOf(attachment), emptyList()).values.first()
|
||||||
SignalDatabase.attachments.finalizeAttachmentAfterUpload(attachmentId, AttachmentTableTestUtil.createUploadResult(attachmentId))
|
SignalDatabase.attachments.finalizeAttachmentAfterUpload(attachmentId, AttachmentTableTestUtil.createUploadResult(attachmentId))
|
||||||
@ -259,7 +259,7 @@ class AttachmentTableTest {
|
|||||||
fun givenAnAttachmentWithAMessageThatExpiresIn5Minutes_whenIGetAttachmentsThatNeedArchiveUpload_thenIDoNotExpectThatAttachment() {
|
fun givenAnAttachmentWithAMessageThatExpiresIn5Minutes_whenIGetAttachmentsThatNeedArchiveUpload_thenIDoNotExpectThatAttachment() {
|
||||||
// GIVEN
|
// GIVEN
|
||||||
val uncompressData = byteArrayOf(1, 2, 3, 4, 5)
|
val uncompressData = byteArrayOf(1, 2, 3, 4, 5)
|
||||||
val blobUncompressed = BlobProvider.getInstance().forData(uncompressData).createForSingleSessionInMemory()
|
val blobUncompressed = AppDependencies.blobs.forData(uncompressData).createForSingleSessionInMemory()
|
||||||
val attachment = createAttachment(1, blobUncompressed, TransformProperties.empty())
|
val attachment = createAttachment(1, blobUncompressed, TransformProperties.empty())
|
||||||
val message = createIncomingMessage(serverTime = 0.days, attachment = attachment, expiresIn = 5.minutes)
|
val message = createIncomingMessage(serverTime = 0.days, attachment = attachment, expiresIn = 5.minutes)
|
||||||
val messageId = SignalDatabase.messages.insertMessageInbox(message).map { it.messageId }.get()
|
val messageId = SignalDatabase.messages.insertMessageInbox(message).map { it.messageId }.get()
|
||||||
@ -278,7 +278,7 @@ class AttachmentTableTest {
|
|||||||
fun givenAnAttachmentWithAMessageThatExpiresIn5Days_whenIGetAttachmentsThatNeedArchiveUpload_thenIDoExpectThatAttachment() {
|
fun givenAnAttachmentWithAMessageThatExpiresIn5Days_whenIGetAttachmentsThatNeedArchiveUpload_thenIDoExpectThatAttachment() {
|
||||||
// GIVEN
|
// GIVEN
|
||||||
val uncompressData = byteArrayOf(1, 2, 3, 4, 5)
|
val uncompressData = byteArrayOf(1, 2, 3, 4, 5)
|
||||||
val blobUncompressed = BlobProvider.getInstance().forData(uncompressData).createForSingleSessionInMemory()
|
val blobUncompressed = AppDependencies.blobs.forData(uncompressData).createForSingleSessionInMemory()
|
||||||
val attachment = createAttachment(1, blobUncompressed, TransformProperties.empty())
|
val attachment = createAttachment(1, blobUncompressed, TransformProperties.empty())
|
||||||
val message = createIncomingMessage(serverTime = 0.days, attachment = attachment, expiresIn = 5.days)
|
val message = createIncomingMessage(serverTime = 0.days, attachment = attachment, expiresIn = 5.days)
|
||||||
val messageId = SignalDatabase.messages.insertMessageInbox(message).map { it.messageId }.get()
|
val messageId = SignalDatabase.messages.insertMessageInbox(message).map { it.messageId }.get()
|
||||||
@ -297,7 +297,7 @@ class AttachmentTableTest {
|
|||||||
fun givenAnAttachmentWithAMessageWithExpirationStartedThatExpiresIn5Days_whenIGetAttachmentsThatNeedArchiveUpload_thenIDoExpectThatAttachment() {
|
fun givenAnAttachmentWithAMessageWithExpirationStartedThatExpiresIn5Days_whenIGetAttachmentsThatNeedArchiveUpload_thenIDoExpectThatAttachment() {
|
||||||
// GIVEN
|
// GIVEN
|
||||||
val uncompressData = byteArrayOf(1, 2, 3, 4, 5)
|
val uncompressData = byteArrayOf(1, 2, 3, 4, 5)
|
||||||
val blobUncompressed = BlobProvider.getInstance().forData(uncompressData).createForSingleSessionInMemory()
|
val blobUncompressed = AppDependencies.blobs.forData(uncompressData).createForSingleSessionInMemory()
|
||||||
val attachment = createAttachment(1, blobUncompressed, TransformProperties.empty())
|
val attachment = createAttachment(1, blobUncompressed, TransformProperties.empty())
|
||||||
val message = createIncomingMessage(serverTime = 0.days, attachment = attachment, expiresIn = 5.days)
|
val message = createIncomingMessage(serverTime = 0.days, attachment = attachment, expiresIn = 5.days)
|
||||||
val messageId = SignalDatabase.messages.insertMessageInbox(message).map { it.messageId }.get()
|
val messageId = SignalDatabase.messages.insertMessageInbox(message).map { it.messageId }.get()
|
||||||
@ -317,7 +317,7 @@ class AttachmentTableTest {
|
|||||||
fun givenAnAttachmentWithALongTextAttachment_whenIGetAttachmentsThatNeedArchiveUpload_thenIDoNotExpectThatAttachment() {
|
fun givenAnAttachmentWithALongTextAttachment_whenIGetAttachmentsThatNeedArchiveUpload_thenIDoNotExpectThatAttachment() {
|
||||||
// GIVEN
|
// GIVEN
|
||||||
val uncompressData = byteArrayOf(1, 2, 3, 4, 5)
|
val uncompressData = byteArrayOf(1, 2, 3, 4, 5)
|
||||||
val blobUncompressed = BlobProvider.getInstance().forData(uncompressData).createForSingleSessionInMemory()
|
val blobUncompressed = AppDependencies.blobs.forData(uncompressData).createForSingleSessionInMemory()
|
||||||
val attachment = createAttachment(1, blobUncompressed, TransformProperties.empty(), contentType = MediaUtil.LONG_TEXT)
|
val attachment = createAttachment(1, blobUncompressed, TransformProperties.empty(), contentType = MediaUtil.LONG_TEXT)
|
||||||
val message = createIncomingMessage(serverTime = 0.days, attachment = attachment)
|
val message = createIncomingMessage(serverTime = 0.days, attachment = attachment)
|
||||||
val messageId = SignalDatabase.messages.insertMessageInbox(message).map { it.messageId }.get()
|
val messageId = SignalDatabase.messages.insertMessageInbox(message).map { it.messageId }.get()
|
||||||
|
|||||||
@ -5,10 +5,10 @@
|
|||||||
|
|
||||||
package org.thoughtcrime.securesms.database
|
package org.thoughtcrime.securesms.database
|
||||||
|
|
||||||
|
import org.signal.core.models.database.AttachmentId
|
||||||
import org.signal.core.util.Base64
|
import org.signal.core.util.Base64
|
||||||
import org.signal.core.util.Util
|
import org.signal.core.util.Util
|
||||||
import org.signal.network.api.AttachmentUploadResult
|
import org.signal.network.api.AttachmentUploadResult
|
||||||
import org.thoughtcrime.securesms.attachments.AttachmentId
|
|
||||||
import org.thoughtcrime.securesms.attachments.Cdn
|
import org.thoughtcrime.securesms.attachments.Cdn
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId
|
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
|
|||||||
@ -12,21 +12,21 @@ import org.junit.Before
|
|||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import org.signal.core.models.ServiceId
|
import org.signal.core.models.ServiceId
|
||||||
|
import org.signal.core.models.database.AttachmentId
|
||||||
import org.signal.core.models.media.TransformProperties
|
import org.signal.core.models.media.TransformProperties
|
||||||
import org.signal.core.util.Base64
|
import org.signal.core.util.Base64
|
||||||
import org.signal.core.util.Util
|
import org.signal.core.util.Util
|
||||||
import org.signal.core.util.readFully
|
import org.signal.core.util.readFully
|
||||||
import org.signal.core.util.stream.LimitedInputStream
|
import org.signal.core.util.stream.LimitedInputStream
|
||||||
import org.signal.core.util.update
|
import org.signal.core.util.update
|
||||||
import org.thoughtcrime.securesms.attachments.AttachmentId
|
import org.signal.mediasend.SentMediaQuality
|
||||||
import org.thoughtcrime.securesms.attachments.Cdn
|
import org.thoughtcrime.securesms.attachments.Cdn
|
||||||
import org.thoughtcrime.securesms.attachments.PointerAttachment
|
import org.thoughtcrime.securesms.attachments.PointerAttachment
|
||||||
|
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
import org.thoughtcrime.securesms.mms.MediaStream
|
import org.thoughtcrime.securesms.mms.MediaStream
|
||||||
import org.thoughtcrime.securesms.mms.OutgoingMessage
|
import org.thoughtcrime.securesms.mms.OutgoingMessage
|
||||||
import org.thoughtcrime.securesms.mms.QuoteModel
|
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.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.util.MediaUtil
|
import org.thoughtcrime.securesms.util.MediaUtil
|
||||||
import org.whispersystems.signalservice.internal.crypto.PaddingInputStream
|
import org.whispersystems.signalservice.internal.crypto.PaddingInputStream
|
||||||
@ -671,7 +671,7 @@ class AttachmentTableTest_deduping {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun insertWithData(data: ByteArray, transformProperties: TransformProperties = TransformProperties.empty()): AttachmentId {
|
fun insertWithData(data: ByteArray, transformProperties: TransformProperties = TransformProperties.empty()): AttachmentId {
|
||||||
val uri = BlobProvider.getInstance().forData(data).createForSingleSessionInMemory()
|
val uri = AppDependencies.blobs.forData(data).createForSingleSessionInMemory()
|
||||||
|
|
||||||
val attachment = UriAttachmentBuilder.build(
|
val attachment = UriAttachmentBuilder.build(
|
||||||
id = Random.nextLong(),
|
id = Random.nextLong(),
|
||||||
|
|||||||
@ -15,14 +15,13 @@ import org.junit.Test
|
|||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import org.signal.core.models.media.TransformProperties
|
import org.signal.core.models.media.TransformProperties
|
||||||
import org.signal.core.util.StreamUtil
|
import org.signal.core.util.StreamUtil
|
||||||
|
import org.signal.mediasend.SentMediaQuality
|
||||||
import org.thoughtcrime.securesms.attachments.UriAttachment
|
import org.thoughtcrime.securesms.attachments.UriAttachment
|
||||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||||
import org.thoughtcrime.securesms.database.UriAttachmentBuilder
|
import org.thoughtcrime.securesms.database.UriAttachmentBuilder
|
||||||
import org.thoughtcrime.securesms.database.transformPropertiesForSentMediaQuality
|
import org.thoughtcrime.securesms.database.transformPropertiesForSentMediaQuality
|
||||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||||
import org.thoughtcrime.securesms.jobmanager.Job
|
import org.thoughtcrime.securesms.jobmanager.Job
|
||||||
import org.thoughtcrime.securesms.mms.SentMediaQuality
|
|
||||||
import org.thoughtcrime.securesms.providers.BlobProvider
|
|
||||||
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||||
import org.thoughtcrime.securesms.util.MediaUtil
|
import org.thoughtcrime.securesms.util.MediaUtil
|
||||||
import java.util.Optional
|
import java.util.Optional
|
||||||
@ -40,7 +39,7 @@ class AttachmentCompressionJobTest {
|
|||||||
StreamUtil.readFully(it)
|
StreamUtil.readFully(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
val blob = BlobProvider.getInstance().forData(imageBytes).createForSingleSessionOnDisk(AppDependencies.application)
|
val blob = AppDependencies.blobs.forData(imageBytes).createForSingleSessionOnDisk(AppDependencies.application)
|
||||||
|
|
||||||
val firstPreUpload = createAttachment(1, blob, TransformProperties.empty())
|
val firstPreUpload = createAttachment(1, blob, TransformProperties.empty())
|
||||||
val firstDatabaseAttachment = SignalDatabase.attachments.insertAttachmentForPreUpload(firstPreUpload)
|
val firstDatabaseAttachment = SignalDatabase.attachments.insertAttachmentForPreUpload(firstPreUpload)
|
||||||
|
|||||||
@ -19,17 +19,31 @@ import org.junit.After
|
|||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
import org.signal.core.util.Base64
|
||||||
|
import org.signal.core.util.Util
|
||||||
import org.signal.network.NetworkResult
|
import org.signal.network.NetworkResult
|
||||||
import org.signal.network.exceptions.NonSuccessfulResponseCodeException
|
import org.signal.network.exceptions.NonSuccessfulResponseCodeException
|
||||||
|
import org.thoughtcrime.securesms.attachments.Cdn
|
||||||
|
import org.thoughtcrime.securesms.attachments.PointerAttachment
|
||||||
import org.thoughtcrime.securesms.backup.DeletionState
|
import org.thoughtcrime.securesms.backup.DeletionState
|
||||||
import org.thoughtcrime.securesms.backup.v2.BackupRepository
|
import org.thoughtcrime.securesms.backup.v2.BackupRepository
|
||||||
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
|
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
|
||||||
|
import org.thoughtcrime.securesms.database.AttachmentTable
|
||||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||||
import org.thoughtcrime.securesms.jobs.protos.BackupDeleteJobData
|
import org.thoughtcrime.securesms.jobs.protos.BackupDeleteJobData
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
|
import org.thoughtcrime.securesms.testing.Flag
|
||||||
|
import org.thoughtcrime.securesms.testing.RemoteConfigForTest
|
||||||
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||||
import org.thoughtcrime.securesms.util.RemoteConfig
|
import org.thoughtcrime.securesms.testing.TestRemoteConfigFlag
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
@RemoteConfigForTest(
|
||||||
|
flags = [
|
||||||
|
Flag(TestRemoteConfigFlag.INTERNAL_USER, "true"),
|
||||||
|
Flag(TestRemoteConfigFlag.DEFAULT_MAX_BACKOFF, "1")
|
||||||
|
]
|
||||||
|
)
|
||||||
class BackupDeleteJobTest {
|
class BackupDeleteJobTest {
|
||||||
|
|
||||||
@get:Rule
|
@get:Rule
|
||||||
@ -37,10 +51,6 @@ class BackupDeleteJobTest {
|
|||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setUp() {
|
fun setUp() {
|
||||||
mockkObject(RemoteConfig)
|
|
||||||
every { RemoteConfig.internalUser } returns true
|
|
||||||
every { RemoteConfig.defaultMaxBackoff } returns 1000L
|
|
||||||
|
|
||||||
mockkObject(BackupRepository)
|
mockkObject(BackupRepository)
|
||||||
every { BackupRepository.getBackupTier() } returns NetworkResult.Success(MessageBackupTier.PAID)
|
every { BackupRepository.getBackupTier() } returns NetworkResult.Success(MessageBackupTier.PAID)
|
||||||
every { BackupRepository.deleteBackup() } returns NetworkResult.Success(Unit)
|
every { BackupRepository.deleteBackup() } returns NetworkResult.Success(Unit)
|
||||||
@ -54,29 +64,24 @@ class BackupDeleteJobTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun givenUserNotRegistered_whenIRun_thenIExpectFailure() {
|
fun givenUserNotRegistered_whenIRun_thenIExpectFailure() {
|
||||||
mockkObject(SignalStore) {
|
SignalStore.account.setRegistered(false)
|
||||||
every { SignalStore.account.isRegistered } returns false
|
|
||||||
|
|
||||||
val job = BackupDeleteJob()
|
val job = BackupDeleteJob()
|
||||||
|
|
||||||
val result = job.run()
|
val result = job.run()
|
||||||
|
|
||||||
assertThat(result.isFailure).isTrue()
|
assertThat(result.isFailure).isTrue()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun givenLinkedDevice_whenIRun_thenIExpectFailure() {
|
fun givenLinkedDevice_whenIRun_thenIExpectFailure() {
|
||||||
mockkObject(SignalStore) {
|
SignalStore.account.deviceId = 2
|
||||||
every { SignalStore.account.isRegistered } returns true
|
|
||||||
every { SignalStore.account.isLinkedDevice } returns true
|
|
||||||
|
|
||||||
val job = BackupDeleteJob()
|
val job = BackupDeleteJob()
|
||||||
|
|
||||||
val result = job.run()
|
val result = job.run()
|
||||||
|
|
||||||
assertThat(result.isFailure).isTrue()
|
assertThat(result.isFailure).isTrue()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -155,10 +160,7 @@ class BackupDeleteJobTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun givenMediaOffloaded_whenIRun_thenIExpectAwaitingMediaDownload() {
|
fun givenMediaOffloaded_whenIRun_thenIExpectAwaitingMediaDownload() {
|
||||||
mockkObject(SignalDatabase)
|
insertOffloadedAttachment()
|
||||||
every { SignalDatabase.attachments.getRemainingRestorableAttachmentSize() } returns 1
|
|
||||||
every { SignalDatabase.attachments.getOptimizedMediaAttachmentSize() } returns 1
|
|
||||||
every { SignalDatabase.attachments.clearAllArchiveData() } returns Unit
|
|
||||||
|
|
||||||
SignalStore.backup.deletionState = DeletionState.CLEAR_LOCAL_STATE
|
SignalStore.backup.deletionState = DeletionState.CLEAR_LOCAL_STATE
|
||||||
|
|
||||||
@ -252,4 +254,39 @@ class BackupDeleteJobTest {
|
|||||||
|
|
||||||
assertThat(result.isRetry).isTrue()
|
assertThat(result.isRetry).isTrue()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun insertOffloadedAttachment(size: Long = 100) {
|
||||||
|
SignalDatabase.attachments.insertAttachmentsForMessage(
|
||||||
|
mmsId = 1,
|
||||||
|
attachments = listOf(
|
||||||
|
PointerAttachment(
|
||||||
|
contentType = "image/jpeg",
|
||||||
|
transferState = AttachmentTable.TRANSFER_RESTORE_OFFLOADED,
|
||||||
|
size = size,
|
||||||
|
fileName = null,
|
||||||
|
cdn = Cdn.CDN_3,
|
||||||
|
location = "somelocation",
|
||||||
|
key = Base64.encodeWithPadding(Util.getSecretBytes(64)),
|
||||||
|
iv = null,
|
||||||
|
digest = Util.getSecretBytes(64),
|
||||||
|
incrementalDigest = null,
|
||||||
|
incrementalMacChunkSize = 0,
|
||||||
|
fastPreflightId = null,
|
||||||
|
voiceNote = false,
|
||||||
|
borderless = false,
|
||||||
|
videoGif = false,
|
||||||
|
width = 100,
|
||||||
|
height = 100,
|
||||||
|
uploadTimestamp = System.currentTimeMillis(),
|
||||||
|
caption = null,
|
||||||
|
stickerLocator = null,
|
||||||
|
blurHash = null,
|
||||||
|
uuid = UUID.randomUUID(),
|
||||||
|
quote = false,
|
||||||
|
quoteTargetContentType = null
|
||||||
|
)
|
||||||
|
),
|
||||||
|
quoteAttachment = emptyList()
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -42,8 +42,10 @@ import org.thoughtcrime.securesms.dependencies.AppDependencies
|
|||||||
import org.thoughtcrime.securesms.jobmanager.Job
|
import org.thoughtcrime.securesms.jobmanager.Job
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
import org.thoughtcrime.securesms.net.SignalNetwork
|
import org.thoughtcrime.securesms.net.SignalNetwork
|
||||||
|
import org.thoughtcrime.securesms.testing.Flag
|
||||||
|
import org.thoughtcrime.securesms.testing.RemoteConfigForTest
|
||||||
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||||
import org.thoughtcrime.securesms.util.RemoteConfig
|
import org.thoughtcrime.securesms.testing.TestRemoteConfigFlag
|
||||||
import org.whispersystems.signalservice.api.storage.IAPSubscriptionId
|
import org.whispersystems.signalservice.api.storage.IAPSubscriptionId
|
||||||
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription
|
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription
|
||||||
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription.ChargeFailure
|
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription.ChargeFailure
|
||||||
@ -55,6 +57,7 @@ import java.util.Currency
|
|||||||
import kotlin.time.Duration.Companion.days
|
import kotlin.time.Duration.Companion.days
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
|
@RemoteConfigForTest(flags = [Flag(TestRemoteConfigFlag.INTERNAL_USER, "true") ])
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
class BackupSubscriptionCheckJobTest {
|
class BackupSubscriptionCheckJobTest {
|
||||||
|
|
||||||
@ -67,9 +70,6 @@ class BackupSubscriptionCheckJobTest {
|
|||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setUp() {
|
fun setUp() {
|
||||||
mockkObject(RemoteConfig)
|
|
||||||
every { RemoteConfig.internalUser } returns true
|
|
||||||
|
|
||||||
coEvery { AppDependencies.billingApi.getApiAvailability() } returns BillingResponseCode.OK
|
coEvery { AppDependencies.billingApi.getApiAvailability() } returns BillingResponseCode.OK
|
||||||
|
|
||||||
coEvery { AppDependencies.billingApi.queryPurchases() } returns BillingPurchaseResult.Success(
|
coEvery { AppDependencies.billingApi.queryPurchases() } returns BillingPurchaseResult.Success(
|
||||||
@ -142,26 +142,22 @@ class BackupSubscriptionCheckJobTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun givenUserIsNotRegistered_whenIRun_thenIExpectSuccessAndEarlyExit() {
|
fun givenUserIsNotRegistered_whenIRun_thenIExpectSuccessAndEarlyExit() {
|
||||||
mockkObject(SignalStore.account) {
|
SignalStore.account.setRegistered(false)
|
||||||
every { SignalStore.account.isRegistered } returns false
|
|
||||||
|
|
||||||
val job = BackupSubscriptionCheckJob.create()
|
val job = BackupSubscriptionCheckJob.create()
|
||||||
val result = job.run()
|
val result = job.run()
|
||||||
|
|
||||||
assertEarlyExit(result)
|
assertEarlyExit(result)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun givenIsLinkedDevice_whenIRun_thenIExpectSuccessAndEarlyExit() {
|
fun givenIsLinkedDevice_whenIRun_thenIExpectSuccessAndEarlyExit() {
|
||||||
mockkObject(SignalStore.account) {
|
SignalStore.account.deviceId = 2
|
||||||
every { SignalStore.account.isLinkedDevice } returns true
|
|
||||||
|
|
||||||
val job = BackupSubscriptionCheckJob.create()
|
val job = BackupSubscriptionCheckJob.create()
|
||||||
val result = job.run()
|
val result = job.run()
|
||||||
|
|
||||||
assertEarlyExit(result)
|
assertEarlyExit(result)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@ -32,8 +32,8 @@ import org.thoughtcrime.securesms.conversation.ConversationIntents
|
|||||||
import org.thoughtcrime.securesms.conversation.v2.ConversationFragment
|
import org.thoughtcrime.securesms.conversation.v2.ConversationFragment
|
||||||
import org.thoughtcrime.securesms.conversationlist.ConversationListArchiveFragment
|
import org.thoughtcrime.securesms.conversationlist.ConversationListArchiveFragment
|
||||||
import org.thoughtcrime.securesms.conversationlist.ConversationListFragment
|
import org.thoughtcrime.securesms.conversationlist.ConversationListFragment
|
||||||
|
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||||
import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionActivity
|
import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionActivity
|
||||||
import org.thoughtcrime.securesms.providers.BlobProvider
|
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||||
import org.thoughtcrime.securesms.stories.landing.StoriesLandingFragment
|
import org.thoughtcrime.securesms.stories.landing.StoriesLandingFragment
|
||||||
@ -348,11 +348,13 @@ class MainNavigationLaunchTest {
|
|||||||
await(description = "no new ConversationFragment after Empty detail intent") {
|
await(description = "no new ConversationFragment after Empty detail intent") {
|
||||||
recorder.createdArgs.size == baseline
|
recorder.createdArgs.size == baseline
|
||||||
}
|
}
|
||||||
// The user-visible signal that we're "back on the list" is the chat list fragment
|
|
||||||
// being attached, not just the VM saying CHATS.
|
|
||||||
awaitListFragment(launched, MainNavigationListLocation.CHATS)
|
|
||||||
|
|
||||||
val vm = runOnMainSync { launched.activity.mainNavigationViewModel() }
|
val vm = runOnMainSync { launched.activity.mainNavigationViewModel() }
|
||||||
|
|
||||||
|
await(description = "conversation cleared from chats back stack after Empty detail intent") {
|
||||||
|
vm.chatsBackStackEntries.none { it is MainNavigationDetailLocation.Conversation }
|
||||||
|
}
|
||||||
|
|
||||||
check(vm.mainNavigationState.value.currentListLocation == MainNavigationListLocation.CHATS) {
|
check(vm.mainNavigationState.value.currentListLocation == MainNavigationListLocation.CHATS) {
|
||||||
"Expected CHATS, got ${vm.mainNavigationState.value.currentListLocation}"
|
"Expected CHATS, got ${vm.mainNavigationState.value.currentListLocation}"
|
||||||
}
|
}
|
||||||
@ -569,7 +571,7 @@ class MainNavigationLaunchTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun realBlob(bytes: ByteArray, mimeType: String): Uri {
|
private fun realBlob(bytes: ByteArray, mimeType: String): Uri {
|
||||||
return BlobProvider.getInstance()
|
return AppDependencies.blobs
|
||||||
.forData(bytes)
|
.forData(bytes)
|
||||||
.withMimeType(mimeType)
|
.withMimeType(mimeType)
|
||||||
.createForSingleSessionInMemory()
|
.createForSingleSessionInMemory()
|
||||||
|
|||||||
@ -18,10 +18,10 @@ import org.thoughtcrime.securesms.database.UriAttachmentBuilder
|
|||||||
import org.thoughtcrime.securesms.database.model.GroupsV2UpdateMessageConverter
|
import org.thoughtcrime.securesms.database.model.GroupsV2UpdateMessageConverter
|
||||||
import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context
|
import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context
|
||||||
import org.thoughtcrime.securesms.database.model.databaseprotos.GV2UpdateDescription
|
import org.thoughtcrime.securesms.database.model.databaseprotos.GV2UpdateDescription
|
||||||
|
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||||
import org.thoughtcrime.securesms.jobs.ThreadUpdateJob
|
import org.thoughtcrime.securesms.jobs.ThreadUpdateJob
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
import org.thoughtcrime.securesms.mms.OutgoingMessage
|
import org.thoughtcrime.securesms.mms.OutgoingMessage
|
||||||
import org.thoughtcrime.securesms.providers.BlobProvider
|
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||||
import org.thoughtcrime.securesms.testing.GroupTestingUtils
|
import org.thoughtcrime.securesms.testing.GroupTestingUtils
|
||||||
@ -120,7 +120,7 @@ class MessageHelper(private val harness: SignalActivityRule, var startTime: Long
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun outgoingAttachment(data: ByteArray, uuid: UUID? = UUID.randomUUID()): Attachment {
|
fun outgoingAttachment(data: ByteArray, uuid: UUID? = UUID.randomUUID()): Attachment {
|
||||||
val uri: Uri = BlobProvider.getInstance().forData(data).createForSingleSessionInMemory()
|
val uri: Uri = AppDependencies.blobs.forData(data).createForSingleSessionInMemory()
|
||||||
|
|
||||||
val attachment: UriAttachment = UriAttachmentBuilder.build(
|
val attachment: UriAttachment = UriAttachmentBuilder.build(
|
||||||
id = Random.nextLong(),
|
id = Random.nextLong(),
|
||||||
|
|||||||
@ -0,0 +1,338 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2026 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.thoughtcrime.securesms.messages
|
||||||
|
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import assertk.assertThat
|
||||||
|
import assertk.assertions.isEqualTo
|
||||||
|
import assertk.assertions.isNotNull
|
||||||
|
import okio.ByteString.Companion.toByteString
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.signal.core.models.database.AttachmentId
|
||||||
|
import org.signal.core.util.Base64
|
||||||
|
import org.thoughtcrime.securesms.database.AttachmentTable
|
||||||
|
import org.thoughtcrime.securesms.database.MessageTable
|
||||||
|
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||||
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
|
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||||
|
import org.thoughtcrime.securesms.testing.MessageContentFuzzer
|
||||||
|
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||||
|
import org.thoughtcrime.securesms.util.MediaUtil
|
||||||
|
import org.whispersystems.signalservice.api.push.SignalServiceAddress
|
||||||
|
import org.whispersystems.signalservice.internal.push.AddressableMessage
|
||||||
|
import org.whispersystems.signalservice.internal.push.AttachmentPointer
|
||||||
|
import org.whispersystems.signalservice.internal.push.Content
|
||||||
|
import org.whispersystems.signalservice.internal.push.ConversationIdentifier
|
||||||
|
import org.whispersystems.signalservice.internal.push.DataMessage
|
||||||
|
import org.whispersystems.signalservice.internal.push.SyncMessage
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
@Suppress("ClassName")
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class SyncMessageProcessorTest_attachmentBackfill {
|
||||||
|
|
||||||
|
@get:Rule
|
||||||
|
val harness = SignalActivityRule(createGroup = true)
|
||||||
|
|
||||||
|
private lateinit var messageHelper: MessageHelper
|
||||||
|
private var originalDeviceId: Int = SignalServiceAddress.DEFAULT_DEVICE_ID
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
messageHelper = MessageHelper(harness)
|
||||||
|
originalDeviceId = SignalStore.account.deviceId
|
||||||
|
// Make this device a linked device so backfill response handling activates.
|
||||||
|
SignalStore.account.deviceId = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun tearDown() {
|
||||||
|
SignalStore.account.deviceId = originalDeviceId
|
||||||
|
messageHelper.tearDown()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun fresh_pointer_updates_row_and_resets_transfer_state() {
|
||||||
|
val (messageId, attachmentId) = insertIncomingMediaMessage(messageHelper.alice)
|
||||||
|
SignalDatabase.attachments.setTransferProgressFailed(attachmentId, messageId)
|
||||||
|
|
||||||
|
val pointer = freshPointer(cdnNumber = 3, cdnKey = "fresh-key", size = 1234, uploadTimestamp = 9_999_000L)
|
||||||
|
deliverBackfillResponse(
|
||||||
|
sender = messageHelper.alice,
|
||||||
|
sentTimestamp = sentTimestampFor(messageId),
|
||||||
|
conversationId = messageHelper.alice,
|
||||||
|
attachmentData = listOf(SyncMessage.AttachmentBackfillResponse.AttachmentData(attachment = pointer))
|
||||||
|
)
|
||||||
|
|
||||||
|
// transferState is not asserted: the forced download job's onAdded() races it PENDING -> STARTED. The pointer fields
|
||||||
|
// are written synchronously and are stable.
|
||||||
|
val refreshed = SignalDatabase.attachments.getAttachmentsForMessage(messageId).single()
|
||||||
|
assertThat(refreshed.remoteLocation).isEqualTo("fresh-key")
|
||||||
|
assertThat(refreshed.cdn.cdnNumber).isEqualTo(3)
|
||||||
|
assertThat(refreshed.size).isEqualTo(1234L)
|
||||||
|
assertThat(refreshed.uploadTimestamp).isEqualTo(9_999_000L)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun terminal_error_marks_permanent_failure() {
|
||||||
|
val (messageId, attachmentId) = insertIncomingMediaMessage(messageHelper.alice)
|
||||||
|
SignalDatabase.attachments.setTransferProgressFailed(attachmentId, messageId)
|
||||||
|
|
||||||
|
deliverBackfillResponse(
|
||||||
|
sender = messageHelper.alice,
|
||||||
|
sentTimestamp = sentTimestampFor(messageId),
|
||||||
|
conversationId = messageHelper.alice,
|
||||||
|
attachmentData = listOf(
|
||||||
|
SyncMessage.AttachmentBackfillResponse.AttachmentData(
|
||||||
|
status = SyncMessage.AttachmentBackfillResponse.AttachmentData.Status.TERMINAL_ERROR
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val refreshed = SignalDatabase.attachments.getAttachmentsForMessage(messageId).single()
|
||||||
|
assertThat(refreshed.transferState).isEqualTo(AttachmentTable.TRANSFER_PROGRESS_PERMANENT_FAILURE)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun pending_status_leaves_row_unchanged() {
|
||||||
|
val (messageId, attachmentId) = insertIncomingMediaMessage(messageHelper.alice)
|
||||||
|
SignalDatabase.attachments.setTransferProgressFailed(attachmentId, messageId)
|
||||||
|
|
||||||
|
deliverBackfillResponse(
|
||||||
|
sender = messageHelper.alice,
|
||||||
|
sentTimestamp = sentTimestampFor(messageId),
|
||||||
|
conversationId = messageHelper.alice,
|
||||||
|
attachmentData = listOf(
|
||||||
|
SyncMessage.AttachmentBackfillResponse.AttachmentData(
|
||||||
|
status = SyncMessage.AttachmentBackfillResponse.AttachmentData.Status.PENDING
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val refreshed = SignalDatabase.attachments.getAttachmentsForMessage(messageId).single()
|
||||||
|
assertThat(refreshed.transferState).isEqualTo(AttachmentTable.TRANSFER_PROGRESS_FAILED)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun message_not_found_error_marks_attachments_retryable_failed() {
|
||||||
|
val (messageId, attachmentId) = insertIncomingMediaMessage(messageHelper.alice)
|
||||||
|
SignalDatabase.attachments.setTransferProgressFailed(attachmentId, messageId)
|
||||||
|
|
||||||
|
deliverBackfillResponse(
|
||||||
|
sender = messageHelper.alice,
|
||||||
|
sentTimestamp = sentTimestampFor(messageId),
|
||||||
|
conversationId = messageHelper.alice,
|
||||||
|
error = SyncMessage.AttachmentBackfillResponse.Error.MESSAGE_NOT_FOUND
|
||||||
|
)
|
||||||
|
|
||||||
|
val refreshed = SignalDatabase.attachments.getAttachmentsForMessage(messageId).single()
|
||||||
|
assertThat(refreshed.transferState).isEqualTo(AttachmentTable.TRANSFER_PROGRESS_FAILED)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun primary_device_ignores_backfill_response() {
|
||||||
|
SignalStore.account.deviceId = SignalServiceAddress.DEFAULT_DEVICE_ID
|
||||||
|
|
||||||
|
val (messageId, attachmentId) = insertIncomingMediaMessage(messageHelper.alice)
|
||||||
|
SignalDatabase.attachments.setTransferProgressFailed(attachmentId, messageId)
|
||||||
|
|
||||||
|
deliverBackfillResponse(
|
||||||
|
sender = messageHelper.alice,
|
||||||
|
sentTimestamp = sentTimestampFor(messageId),
|
||||||
|
conversationId = messageHelper.alice,
|
||||||
|
attachmentData = listOf(
|
||||||
|
SyncMessage.AttachmentBackfillResponse.AttachmentData(
|
||||||
|
status = SyncMessage.AttachmentBackfillResponse.AttachmentData.Status.TERMINAL_ERROR
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val refreshed = SignalDatabase.attachments.getAttachmentsForMessage(messageId).single()
|
||||||
|
assertThat(refreshed.transferState).isEqualTo(AttachmentTable.TRANSFER_PROGRESS_FAILED)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun multi_attachment_response_matches_positionally_with_mixed_status() {
|
||||||
|
val messageId = insertIncomingMessageWith(messageHelper.alice, listOf(incomingImagePointer(), incomingImagePointer()))
|
||||||
|
val body = SignalDatabase.attachments.getAttachmentsForMessage(messageId).sortedBy { it.displayOrder }
|
||||||
|
assertThat(body.size).isEqualTo(2)
|
||||||
|
body.forEach { SignalDatabase.attachments.setTransferProgressFailed(it.attachmentId, messageId) }
|
||||||
|
|
||||||
|
// Response is a positional array: index 0 -> body[0] (fresh pointer), index 1 -> body[1] (terminal).
|
||||||
|
deliverBackfillResponse(
|
||||||
|
sender = messageHelper.alice,
|
||||||
|
sentTimestamp = sentTimestampFor(messageId),
|
||||||
|
conversationId = messageHelper.alice,
|
||||||
|
attachmentData = listOf(
|
||||||
|
SyncMessage.AttachmentBackfillResponse.AttachmentData(attachment = freshPointer(cdnNumber = 3, cdnKey = "first-key", size = 11, uploadTimestamp = 111L)),
|
||||||
|
SyncMessage.AttachmentBackfillResponse.AttachmentData(status = SyncMessage.AttachmentBackfillResponse.AttachmentData.Status.TERMINAL_ERROR)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val refreshed = SignalDatabase.attachments.getAttachmentsForMessage(messageId).sortedBy { it.displayOrder }
|
||||||
|
// remoteLocation proves index 0 routed to body[0]. transferState is not asserted: it races the download job's onAdded().
|
||||||
|
assertThat(refreshed[0].remoteLocation).isEqualTo("first-key")
|
||||||
|
assertThat(refreshed[0].cdn.cdnNumber).isEqualTo(3)
|
||||||
|
assertThat(refreshed[1].transferState).isEqualTo(AttachmentTable.TRANSFER_PROGRESS_PERMANENT_FAILURE)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun long_text_slot_is_applied_independently_of_the_body() {
|
||||||
|
val messageId = insertIncomingMessageWith(messageHelper.alice, listOf(incomingImagePointer(), incomingLongTextPointer()))
|
||||||
|
val all = SignalDatabase.attachments.getAttachmentsForMessage(messageId)
|
||||||
|
all.forEach { SignalDatabase.attachments.setTransferProgressFailed(it.attachmentId, messageId) }
|
||||||
|
|
||||||
|
deliverBackfillResponse(
|
||||||
|
sender = messageHelper.alice,
|
||||||
|
sentTimestamp = sentTimestampFor(messageId),
|
||||||
|
conversationId = messageHelper.alice,
|
||||||
|
attachmentData = listOf(SyncMessage.AttachmentBackfillResponse.AttachmentData(attachment = freshPointer(cdnNumber = 3, cdnKey = "body-key", size = 22, uploadTimestamp = 222L))),
|
||||||
|
longText = SyncMessage.AttachmentBackfillResponse.AttachmentData(attachment = freshPointer(cdnNumber = 3, cdnKey = "long-text-key", size = 33, uploadTimestamp = 333L))
|
||||||
|
)
|
||||||
|
|
||||||
|
val refreshed = SignalDatabase.attachments.getAttachmentsForMessage(messageId)
|
||||||
|
val bodyRow = refreshed.single { it.contentType != MediaUtil.LONG_TEXT }
|
||||||
|
val longTextRow = refreshed.single { it.contentType == MediaUtil.LONG_TEXT }
|
||||||
|
// The positional `attachments` array fills the body row and the separate `longText` slot fills the long-text row,
|
||||||
|
// with no cross-contamination. transferState is not asserted: it races the download job's onAdded().
|
||||||
|
assertThat(bodyRow.remoteLocation).isEqualTo("body-key")
|
||||||
|
assertThat(longTextRow.remoteLocation).isEqualTo("long-text-key")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun remote_attachment_list_longer_than_local_skips_extras() {
|
||||||
|
val messageId = insertIncomingMessageWith(messageHelper.alice, listOf(incomingImagePointer()))
|
||||||
|
val attachmentId = SignalDatabase.attachments.getAttachmentsForMessage(messageId).single().attachmentId
|
||||||
|
SignalDatabase.attachments.setTransferProgressFailed(attachmentId, messageId)
|
||||||
|
|
||||||
|
deliverBackfillResponse(
|
||||||
|
sender = messageHelper.alice,
|
||||||
|
sentTimestamp = sentTimestampFor(messageId),
|
||||||
|
conversationId = messageHelper.alice,
|
||||||
|
attachmentData = listOf(
|
||||||
|
SyncMessage.AttachmentBackfillResponse.AttachmentData(attachment = freshPointer(cdnNumber = 3, cdnKey = "only-key", size = 44, uploadTimestamp = 444L)),
|
||||||
|
SyncMessage.AttachmentBackfillResponse.AttachmentData(attachment = freshPointer(cdnNumber = 3, cdnKey = "extra-key", size = 55, uploadTimestamp = 555L))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// The single local row is routed from index 0; the extra index-1 entry has no body[1] and must be skipped, not throw.
|
||||||
|
val refreshed = SignalDatabase.attachments.getAttachmentsForMessage(messageId).single()
|
||||||
|
assertThat(refreshed.remoteLocation).isEqualTo("only-key")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun insertIncomingMediaMessage(sender: RecipientId): Pair<Long, AttachmentId> {
|
||||||
|
messageHelper.startTime = messageHelper.nextStartTime()
|
||||||
|
val sentTimestamp = messageHelper.startTime
|
||||||
|
|
||||||
|
val content = Content.Builder()
|
||||||
|
.dataMessage(
|
||||||
|
DataMessage.Builder()
|
||||||
|
.timestamp(sentTimestamp)
|
||||||
|
.attachments(listOf(MessageContentFuzzer.attachmentPointer()))
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
messageHelper.processor.process(
|
||||||
|
envelope = MessageContentFuzzer.envelope(sentTimestamp),
|
||||||
|
content = content,
|
||||||
|
metadata = MessageContentFuzzer.envelopeMetadata(source = sender, destination = harness.self.id),
|
||||||
|
serverDeliveredTimestamp = sentTimestamp + 10
|
||||||
|
)
|
||||||
|
|
||||||
|
val syncMessageId = MessageTable.SyncMessageId(sender, sentTimestamp)
|
||||||
|
val messageId = SignalDatabase.messages.getMessageIdOrNull(syncMessageId)
|
||||||
|
assertThat(messageId, name = "messageId").isNotNull()
|
||||||
|
|
||||||
|
val attachment = SignalDatabase.attachments.getAttachmentsForMessage(messageId!!).single()
|
||||||
|
return messageId to attachment.attachmentId
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun insertIncomingMessageWith(sender: RecipientId, pointers: List<AttachmentPointer>): Long {
|
||||||
|
messageHelper.startTime = messageHelper.nextStartTime()
|
||||||
|
val sentTimestamp = messageHelper.startTime
|
||||||
|
|
||||||
|
val content = Content.Builder()
|
||||||
|
.dataMessage(
|
||||||
|
DataMessage.Builder()
|
||||||
|
.timestamp(sentTimestamp)
|
||||||
|
.attachments(pointers)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
messageHelper.processor.process(
|
||||||
|
envelope = MessageContentFuzzer.envelope(sentTimestamp),
|
||||||
|
content = content,
|
||||||
|
metadata = MessageContentFuzzer.envelopeMetadata(source = sender, destination = harness.self.id),
|
||||||
|
serverDeliveredTimestamp = sentTimestamp + 10
|
||||||
|
)
|
||||||
|
|
||||||
|
val messageId = SignalDatabase.messages.getMessageIdOrNull(MessageTable.SyncMessageId(sender, sentTimestamp))
|
||||||
|
assertThat(messageId, name = "messageId").isNotNull()
|
||||||
|
return messageId!!
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun incomingImagePointer(): AttachmentPointer = MessageContentFuzzer.attachmentPointer().newBuilder().contentType("image/jpeg").build()
|
||||||
|
|
||||||
|
private fun incomingLongTextPointer(): AttachmentPointer = MessageContentFuzzer.attachmentPointer().newBuilder().contentType(MediaUtil.LONG_TEXT).build()
|
||||||
|
|
||||||
|
private fun sentTimestampFor(messageId: Long): Long {
|
||||||
|
return SignalDatabase.messages.getMessageRecord(messageId).dateSent
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun deliverBackfillResponse(
|
||||||
|
sender: RecipientId,
|
||||||
|
sentTimestamp: Long,
|
||||||
|
conversationId: RecipientId,
|
||||||
|
attachmentData: List<SyncMessage.AttachmentBackfillResponse.AttachmentData> = emptyList(),
|
||||||
|
longText: SyncMessage.AttachmentBackfillResponse.AttachmentData? = null,
|
||||||
|
error: SyncMessage.AttachmentBackfillResponse.Error? = null
|
||||||
|
) {
|
||||||
|
messageHelper.startTime = messageHelper.nextStartTime()
|
||||||
|
val envelopeTimestamp = messageHelper.startTime
|
||||||
|
|
||||||
|
val response = SyncMessage.AttachmentBackfillResponse(
|
||||||
|
targetMessage = AddressableMessage(
|
||||||
|
authorServiceIdBinary = Recipient.resolved(sender).requireAci().toByteString(),
|
||||||
|
sentTimestamp = sentTimestamp
|
||||||
|
),
|
||||||
|
targetConversation = ConversationIdentifier(
|
||||||
|
threadServiceIdBinary = Recipient.resolved(conversationId).requireAci().toByteString()
|
||||||
|
),
|
||||||
|
attachments = if (error == null) SyncMessage.AttachmentBackfillResponse.AttachmentDataList(attachments = attachmentData, longText = longText) else null,
|
||||||
|
error = error
|
||||||
|
)
|
||||||
|
|
||||||
|
val content = Content.Builder()
|
||||||
|
.syncMessage(SyncMessage.Builder().attachmentBackfillResponse(response).build())
|
||||||
|
.build()
|
||||||
|
|
||||||
|
messageHelper.processor.process(
|
||||||
|
envelope = MessageContentFuzzer.envelope(envelopeTimestamp, serverGuid = UUID.randomUUID()),
|
||||||
|
content = content,
|
||||||
|
metadata = MessageContentFuzzer.envelopeMetadata(source = harness.self.id, destination = harness.self.id, sourceDeviceId = 1),
|
||||||
|
serverDeliveredTimestamp = envelopeTimestamp + 10
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun freshPointer(cdnNumber: Int, cdnKey: String, size: Int, uploadTimestamp: Long): AttachmentPointer {
|
||||||
|
return AttachmentPointer.Builder()
|
||||||
|
.cdnKey(cdnKey)
|
||||||
|
.cdnNumber(cdnNumber)
|
||||||
|
.key(Base64.decode("AAAAAAAA").toByteString())
|
||||||
|
.digest(ByteArray(32) { it.toByte() }.toByteString())
|
||||||
|
.size(size)
|
||||||
|
.uploadTimestamp(uploadTimestamp)
|
||||||
|
.contentType("image/jpeg")
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -37,6 +37,7 @@ class InAppPaymentsRule : ExternalResource() {
|
|||||||
private fun initialisePutSubscription() {
|
private fun initialisePutSubscription() {
|
||||||
AppDependencies.donationsApi.apply {
|
AppDependencies.donationsApi.apply {
|
||||||
every { putSubscription(any()) } returns NetworkResult.Success(Unit)
|
every { putSubscription(any()) } returns NetworkResult.Success(Unit)
|
||||||
|
every { createSubscriber(any(), any()) } returns NetworkResult.Success(Unit)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,81 @@
|
|||||||
|
package org.thoughtcrime.securesms.testing
|
||||||
|
|
||||||
|
import org.json.JSONObject
|
||||||
|
import org.thoughtcrime.securesms.util.RemoteConfig
|
||||||
|
import kotlin.reflect.KProperty0
|
||||||
|
import kotlin.reflect.jvm.isAccessible
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Declares remote config values a test needs. The [SignalTestRunner] reads this off the
|
||||||
|
* about-to-run test (class and/or method) and stages the values into [TestRemoteConfig], which
|
||||||
|
* [org.thoughtcrime.securesms.SignalInstrumentationApplicationContext] seeds into the real
|
||||||
|
* [RemoteConfig] before the startup `init()` runs.
|
||||||
|
*
|
||||||
|
* Method-level annotations override class-level ones for the same key. Values are strings, matching
|
||||||
|
* how the service delivers config; `"true"`/`"false"` are decoded into real booleans on the way into
|
||||||
|
* the store (same as [org.signal.network.api.RemoteConfigApi]), other values stay strings.
|
||||||
|
*
|
||||||
|
* Prefer the typed [Flag] (which resolves its key from the actual [RemoteConfig] property); use
|
||||||
|
* [RawFlag] for keys that don't have a [TestRemoteConfigFlag] entry.
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
* @RemoteConfigForTest(
|
||||||
|
* flags = [Flag(TestRemoteConfigFlag.INTERNAL_USER, "true")],
|
||||||
|
* rawFlags = [RawFlag("android.someOtherKey", "1")]
|
||||||
|
* )
|
||||||
|
* class MyTest { ... }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
@Retention(AnnotationRetention.RUNTIME)
|
||||||
|
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
|
||||||
|
annotation class RemoteConfigForTest(
|
||||||
|
val flags: Array<Flag> = [],
|
||||||
|
val rawFlags: Array<RawFlag> = []
|
||||||
|
)
|
||||||
|
|
||||||
|
/** A flag whose key is resolved from the referenced [RemoteConfig] property at runtime. */
|
||||||
|
@Retention(AnnotationRetention.RUNTIME)
|
||||||
|
annotation class Flag(val flag: TestRemoteConfigFlag, val value: String)
|
||||||
|
|
||||||
|
/** A flag identified by its raw remote config key, for keys without a [TestRemoteConfigFlag] entry. */
|
||||||
|
@Retention(AnnotationRetention.RUNTIME)
|
||||||
|
annotation class RawFlag(val key: String, val value: String)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Typed handles for remote config flags referenced by tests.
|
||||||
|
*/
|
||||||
|
enum class TestRemoteConfigFlag(private val property: KProperty0<*>) {
|
||||||
|
INTERNAL_USER(RemoteConfig::internalUser),
|
||||||
|
DEFAULT_MAX_BACKOFF(RemoteConfig::defaultMaxBackoff);
|
||||||
|
|
||||||
|
val key: String
|
||||||
|
get() {
|
||||||
|
property.isAccessible = true
|
||||||
|
val delegate = property.getDelegate() ?: error("RemoteConfig.${property.name} has no delegate; only `by remoteX(...)` configs can be referenced by ${TestRemoteConfigFlag::class.simpleName}.")
|
||||||
|
check(delegate is RemoteConfig.Config<*>) {
|
||||||
|
"RemoteConfig.${property.name} delegate is ${delegate::class.simpleName}, not RemoteConfig.Config; cannot resolve its remote config key."
|
||||||
|
}
|
||||||
|
return delegate.key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process-static bridge between [SignalTestRunner] (which knows the running test) and
|
||||||
|
* [org.thoughtcrime.securesms.SignalInstrumentationApplicationContext] (which seeds the config).
|
||||||
|
* Safe because Orchestrator runs each test in a fresh process.
|
||||||
|
*/
|
||||||
|
object TestRemoteConfig {
|
||||||
|
@Volatile
|
||||||
|
var pending: Map<String, Any> = emptyMap()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The staged config as a JSON string ready to write into `SignalStore.remoteConfig`. Mirrors
|
||||||
|
* [org.signal.network.api.RemoteConfigApi]'s decode so `"true"`/`"false"` land as real booleans
|
||||||
|
* (like the server path) while other values stay strings.
|
||||||
|
*/
|
||||||
|
val json: String
|
||||||
|
get() {
|
||||||
|
val decoded = pending.mapValues { (_, value) -> (value as? String)?.lowercase()?.toBooleanStrictOrNull() ?: value }
|
||||||
|
return JSONObject(decoded).toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,15 +2,62 @@ package org.thoughtcrime.securesms.testing
|
|||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.os.Bundle
|
||||||
import androidx.test.runner.AndroidJUnitRunner
|
import androidx.test.runner.AndroidJUnitRunner
|
||||||
import org.thoughtcrime.securesms.SignalInstrumentationApplicationContext
|
import org.thoughtcrime.securesms.SignalInstrumentationApplicationContext
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom runner that replaces application with [SignalInstrumentationApplicationContext].
|
* Custom runner that replaces application with [SignalInstrumentationApplicationContext].
|
||||||
|
*
|
||||||
|
* Before the application is created, it reads any [RemoteConfigForTest] declared on the
|
||||||
|
* about-to-run test (passed by the Orchestrator as the `class` argument, `pkg.Class#method`) and
|
||||||
|
* stages the values in [TestRemoteConfig] so the app can seed them into `RemoteConfig` at startup.
|
||||||
*/
|
*/
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
class SignalTestRunner : AndroidJUnitRunner() {
|
class SignalTestRunner : AndroidJUnitRunner() {
|
||||||
|
override fun onCreate(arguments: Bundle?) {
|
||||||
|
TestRemoteConfig.pending = parseRemoteConfig(arguments?.getString("class"))
|
||||||
|
super.onCreate(arguments)
|
||||||
|
}
|
||||||
|
|
||||||
override fun newApplication(cl: ClassLoader?, className: String?, context: Context?): Application {
|
override fun newApplication(cl: ClassLoader?, className: String?, context: Context?): Application {
|
||||||
return super.newApplication(cl, SignalInstrumentationApplicationContext::class.java.name, context)
|
return super.newApplication(cl, SignalInstrumentationApplicationContext::class.java.name, context)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves [RemoteConfigForTest] annotations from the targeted test(s). [classArg] is the
|
||||||
|
* instrumentation `class` argument: a comma-separated list of `pkg.Class` or `pkg.Class#method`.
|
||||||
|
* Method-level flags override class-level flags for the same key. Reflection failures (e.g. a
|
||||||
|
* whole-suite run with no `class` arg) fall back to no overrides.
|
||||||
|
*/
|
||||||
|
private fun parseRemoteConfig(classArg: String?): Map<String, Any> {
|
||||||
|
if (classArg.isNullOrBlank()) {
|
||||||
|
return emptyMap()
|
||||||
|
}
|
||||||
|
|
||||||
|
val flags = mutableMapOf<String, Any>()
|
||||||
|
|
||||||
|
for (entry in classArg.split(",")) {
|
||||||
|
val (className, methodName) = entry.trim().split("#", limit = 2).let { it[0] to it.getOrNull(1) }
|
||||||
|
|
||||||
|
try {
|
||||||
|
// initialize = false: only read annotations, don't run the test class's static init this early.
|
||||||
|
val testClass = Class.forName(className, false, javaClass.classLoader)
|
||||||
|
val method = methodName?.let { name -> testClass.declaredMethods.firstOrNull { it.name == name } }
|
||||||
|
|
||||||
|
// Class annotation first, then method annotation so method-level flags override class-level ones.
|
||||||
|
listOfNotNull(
|
||||||
|
testClass.getAnnotation(RemoteConfigForTest::class.java),
|
||||||
|
method?.getAnnotation(RemoteConfigForTest::class.java)
|
||||||
|
).forEach { annotation ->
|
||||||
|
annotation.flags.forEach { flags[it.flag.key] = it.value }
|
||||||
|
annotation.rawFlags.forEach { flags[it.key] = it.value }
|
||||||
|
}
|
||||||
|
} catch (_: ReflectiveOperationException) {
|
||||||
|
// Class/method not resolvable in this run; leave overrides as-is.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return flags
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -32,6 +32,14 @@ class BenchmarkSetupActivity : BaseActivity() {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val TAG = Log.tag(BenchmarkSetupActivity::class)
|
private val TAG = Log.tag(BenchmarkSetupActivity::class)
|
||||||
|
|
||||||
|
const val SEARCH_KEYWORD = "lighthouse"
|
||||||
|
|
||||||
|
private val SEARCH_VOCABULARY = listOf(
|
||||||
|
"hello", "world", "signal", "android", "kotlin", "database", "benchmark", "conversation",
|
||||||
|
"morning", "evening", "weekend", "project", "meeting", "dinner", "coffee", "garden",
|
||||||
|
"mountain", "river", "forest", "harbor", "market", "library", "concert", "holiday"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
@ -51,6 +59,7 @@ class BenchmarkSetupActivity : BaseActivity() {
|
|||||||
when (intent.extras!!.getString("setup-type")) {
|
when (intent.extras!!.getString("setup-type")) {
|
||||||
"cold-start" -> setupColdStart()
|
"cold-start" -> setupColdStart()
|
||||||
"conversation-open" -> setupConversationOpen()
|
"conversation-open" -> setupConversationOpen()
|
||||||
|
"conversation-list-search" -> setupConversationListSearch()
|
||||||
"message-send" -> setupMessageSend()
|
"message-send" -> setupMessageSend()
|
||||||
"group-message-send" -> setupGroupMessageSend()
|
"group-message-send" -> setupGroupMessageSend()
|
||||||
"group-delivery-receipt" -> setupGroupReceipt(includeMsl = true)
|
"group-delivery-receipt" -> setupGroupReceipt(includeMsl = true)
|
||||||
@ -97,6 +106,39 @@ class BenchmarkSetupActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun setupConversationListSearch() {
|
||||||
|
TestUsers.setupSelf()
|
||||||
|
|
||||||
|
val recipientCount = 50
|
||||||
|
val messagesPerRecipient = 2000
|
||||||
|
val totalMessages = recipientCount * messagesPerRecipient
|
||||||
|
val generator = TestMessages.TimestampGenerator(System.currentTimeMillis() - (totalMessages * 2000L) - 60_000L)
|
||||||
|
|
||||||
|
TestUsers.setupTestRecipients(recipientCount).forEachIndexed { recipientIndex, recipientId ->
|
||||||
|
val recipient: Recipient = Recipient.resolved(recipientId)
|
||||||
|
|
||||||
|
for (i in 0 until messagesPerRecipient) {
|
||||||
|
val body = searchableMessageBody(recipientIndex, i)
|
||||||
|
if (i % 2 == 0) {
|
||||||
|
TestMessages.insertIncomingTextMessage(other = recipient, body = body, timestamp = generator.nextTimestamp())
|
||||||
|
} else {
|
||||||
|
TestMessages.insertOutgoingTextMessage(other = recipient, body = body, timestamp = generator.nextTimestamp())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SignalDatabase.messages.setAllMessagesRead()
|
||||||
|
SignalDatabase.threads.update(SignalDatabase.threads.getOrCreateThreadIdFor(recipient = recipient), true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun searchableMessageBody(recipientIndex: Int, messageIndex: Int): String {
|
||||||
|
val words = SEARCH_VOCABULARY
|
||||||
|
val w1 = words[(recipientIndex + messageIndex) % words.size]
|
||||||
|
val w2 = words[(recipientIndex * 7 + messageIndex * 3) % words.size]
|
||||||
|
val w3 = words[(recipientIndex * 13 + messageIndex * 5) % words.size]
|
||||||
|
return "$w1 $w2 $SEARCH_KEYWORD $w3 message $messageIndex"
|
||||||
|
}
|
||||||
|
|
||||||
private fun setupMessageSend() {
|
private fun setupMessageSend() {
|
||||||
TestUsers.setupSelf()
|
TestUsers.setupSelf()
|
||||||
TestUsers.setupTestClients(1)
|
TestUsers.setupTestClients(1)
|
||||||
|
|||||||
@ -241,5 +241,6 @@ class OtherClient(val serviceId: ServiceId, val e164: String, val identityKeyPai
|
|||||||
override fun deleteAllStaleOneTimeKyberPreKeys(threshold: Long, minCount: Int) = throw UnsupportedOperationException()
|
override fun deleteAllStaleOneTimeKyberPreKeys(threshold: Long, minCount: Int) = throw UnsupportedOperationException()
|
||||||
override fun loadLastResortKyberPreKeys(): List<KyberPreKeyRecord> = throw UnsupportedOperationException()
|
override fun loadLastResortKyberPreKeys(): List<KyberPreKeyRecord> = throw UnsupportedOperationException()
|
||||||
override fun isMultiDevice(): Boolean = throw UnsupportedOperationException()
|
override fun isMultiDevice(): Boolean = throw UnsupportedOperationException()
|
||||||
|
override fun setMultiDevice(isMultiDevice: Boolean) = throw UnsupportedOperationException()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -14,7 +14,7 @@ object AppCapabilities {
|
|||||||
versionedExpirationTimer = true,
|
versionedExpirationTimer = true,
|
||||||
attachmentBackfill = true,
|
attachmentBackfill = true,
|
||||||
spqr = true,
|
spqr = true,
|
||||||
usernameChangeSyncMessage = false // TODO(michelle): Turn on once all clients support it and add a migration
|
usernameChangeSyncMessage = true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -39,6 +39,7 @@ import org.signal.core.util.MemoryTracker;
|
|||||||
import org.signal.core.util.Util;
|
import org.signal.core.util.Util;
|
||||||
import org.signal.core.util.concurrent.AnrDetector;
|
import org.signal.core.util.concurrent.AnrDetector;
|
||||||
import org.signal.core.util.concurrent.SignalExecutors;
|
import org.signal.core.util.concurrent.SignalExecutors;
|
||||||
|
import org.signal.core.util.crypto.AttachmentSecretProvider;
|
||||||
import org.signal.core.util.logging.AndroidLogger;
|
import org.signal.core.util.logging.AndroidLogger;
|
||||||
import org.signal.core.util.logging.Log;
|
import org.signal.core.util.logging.Log;
|
||||||
import org.signal.core.util.logging.Scrubber;
|
import org.signal.core.util.logging.Scrubber;
|
||||||
@ -46,14 +47,15 @@ import org.signal.core.util.tracing.Tracer;
|
|||||||
import org.signal.glide.SignalGlideCodecs;
|
import org.signal.glide.SignalGlideCodecs;
|
||||||
import org.signal.libsignal.net.ChatServiceException;
|
import org.signal.libsignal.net.ChatServiceException;
|
||||||
import org.signal.libsignal.protocol.logging.SignalProtocolLoggerProvider;
|
import org.signal.libsignal.protocol.logging.SignalProtocolLoggerProvider;
|
||||||
|
import org.signal.registration.RegistrationDependencies;
|
||||||
import org.signal.ringrtc.CallManager;
|
import org.signal.ringrtc.CallManager;
|
||||||
import org.thoughtcrime.securesms.apkupdate.ApkUpdateRefreshListener;
|
import org.thoughtcrime.securesms.apkupdate.ApkUpdateRefreshListener;
|
||||||
import org.thoughtcrime.securesms.avatar.AvatarPickerStorage;
|
import org.thoughtcrime.securesms.avatar.AvatarPickerStorage;
|
||||||
import org.thoughtcrime.securesms.backup.v2.BackupRepository;
|
import org.thoughtcrime.securesms.backup.v2.BackupRepository;
|
||||||
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider;
|
import org.thoughtcrime.securesms.conversation.drafts.DraftBlobs;
|
||||||
|
import org.thoughtcrime.securesms.crypto.AppAttachmentSecretStore;
|
||||||
import org.thoughtcrime.securesms.crypto.DatabaseSecretProvider;
|
import org.thoughtcrime.securesms.crypto.DatabaseSecretProvider;
|
||||||
import org.thoughtcrime.securesms.database.LogDatabase;
|
import org.thoughtcrime.securesms.database.LogDatabase;
|
||||||
import org.thoughtcrime.securesms.database.SQLiteDatabase;
|
|
||||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||||
import org.thoughtcrime.securesms.database.SqlCipherLibraryLoader;
|
import org.thoughtcrime.securesms.database.SqlCipherLibraryLoader;
|
||||||
import org.thoughtcrime.securesms.dependencies.AppDependencies;
|
import org.thoughtcrime.securesms.dependencies.AppDependencies;
|
||||||
@ -98,10 +100,11 @@ import org.thoughtcrime.securesms.messageprocessingalarm.RoutineMessageFetchRece
|
|||||||
import org.thoughtcrime.securesms.messages.IncomingMessageObserver;
|
import org.thoughtcrime.securesms.messages.IncomingMessageObserver;
|
||||||
import org.thoughtcrime.securesms.migrations.ApplicationMigrations;
|
import org.thoughtcrime.securesms.migrations.ApplicationMigrations;
|
||||||
import org.thoughtcrime.securesms.mms.SignalGlideModule;
|
import org.thoughtcrime.securesms.mms.SignalGlideModule;
|
||||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
|
||||||
import org.thoughtcrime.securesms.ratelimit.RateLimitUtil;
|
import org.thoughtcrime.securesms.ratelimit.RateLimitUtil;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.registration.util.RegistrationUtil;
|
import org.thoughtcrime.securesms.registration.util.RegistrationUtil;
|
||||||
|
import org.thoughtcrime.securesms.registration.v2.AppRegistrationNetworkController;
|
||||||
|
import org.thoughtcrime.securesms.registration.v2.AppRegistrationStorageController;
|
||||||
import org.thoughtcrime.securesms.ringrtc.RingRtcLogger;
|
import org.thoughtcrime.securesms.ringrtc.RingRtcLogger;
|
||||||
import org.thoughtcrime.securesms.service.AnalyzeDatabaseAlarmListener;
|
import org.thoughtcrime.securesms.service.AnalyzeDatabaseAlarmListener;
|
||||||
import org.thoughtcrime.securesms.service.DirectoryRefreshListener;
|
import org.thoughtcrime.securesms.service.DirectoryRefreshListener;
|
||||||
@ -114,6 +117,7 @@ import org.thoughtcrime.securesms.service.webrtc.ActiveCallManager;
|
|||||||
import org.thoughtcrime.securesms.service.webrtc.AndroidTelecomUtil;
|
import org.thoughtcrime.securesms.service.webrtc.AndroidTelecomUtil;
|
||||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
|
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
|
||||||
import org.thoughtcrime.securesms.util.AppStartup;
|
import org.thoughtcrime.securesms.util.AppStartup;
|
||||||
|
import org.thoughtcrime.securesms.util.BatterySnapshotTracker;
|
||||||
import org.thoughtcrime.securesms.util.DeviceProperties;
|
import org.thoughtcrime.securesms.util.DeviceProperties;
|
||||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||||
import org.thoughtcrime.securesms.util.Environment;
|
import org.thoughtcrime.securesms.util.Environment;
|
||||||
@ -171,7 +175,7 @@ public class ApplicationContext extends Application implements AppForegroundObse
|
|||||||
SqlCipherLibraryLoader.load();
|
SqlCipherLibraryLoader.load();
|
||||||
SignalDatabase.init(this,
|
SignalDatabase.init(this,
|
||||||
DatabaseSecretProvider.getOrCreateDatabaseSecret(this),
|
DatabaseSecretProvider.getOrCreateDatabaseSecret(this),
|
||||||
AttachmentSecretProvider.getInstance(this).getOrCreateAttachmentSecret());
|
AttachmentSecretProvider.getInstance(this, AppAttachmentSecretStore.INSTANCE).getOrCreateAttachmentSecret());
|
||||||
Logger.setTarget(SqlCipherLogTarget.INSTANCE);
|
Logger.setTarget(SqlCipherLogTarget.INSTANCE);
|
||||||
})
|
})
|
||||||
.addBlocking("signal-store", () -> SignalStore.init(this))
|
.addBlocking("signal-store", () -> SignalStore.init(this))
|
||||||
@ -259,6 +263,8 @@ public class ApplicationContext extends Application implements AppForegroundObse
|
|||||||
long startTime = System.currentTimeMillis();
|
long startTime = System.currentTimeMillis();
|
||||||
Log.i(TAG, "App is now visible. Battery: " + DeviceProperties.getBatteryLevel(this) + "% (charging: " + DeviceProperties.isCharging(this) + ")");
|
Log.i(TAG, "App is now visible. Battery: " + DeviceProperties.getBatteryLevel(this) + "% (charging: " + DeviceProperties.isCharging(this) + ")");
|
||||||
|
|
||||||
|
BatterySnapshotTracker.emit(this, "foreground");
|
||||||
|
|
||||||
AppDependencies.getFrameRateTracker().start();
|
AppDependencies.getFrameRateTracker().start();
|
||||||
AppDependencies.getMegaphoneRepository().onAppForegrounded();
|
AppDependencies.getMegaphoneRepository().onAppForegrounded();
|
||||||
AppDependencies.getDeadlockDetector().start();
|
AppDependencies.getDeadlockDetector().start();
|
||||||
@ -299,6 +305,7 @@ public class ApplicationContext extends Application implements AppForegroundObse
|
|||||||
@Override
|
@Override
|
||||||
public void onBackground() {
|
public void onBackground() {
|
||||||
Log.i(TAG, "App is no longer visible.");
|
Log.i(TAG, "App is no longer visible.");
|
||||||
|
BatterySnapshotTracker.emit(this, "background");
|
||||||
KeyCachingService.onAppBackgrounded(this);
|
KeyCachingService.onAppBackgrounded(this);
|
||||||
AppDependencies.getMessageNotifier().clearVisibleThread();
|
AppDependencies.getMessageNotifier().clearVisibleThread();
|
||||||
AppDependencies.getFrameRateTracker().stop();
|
AppDependencies.getFrameRateTracker().stop();
|
||||||
@ -417,10 +424,10 @@ public class ApplicationContext extends Application implements AppForegroundObse
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void initializeRegistrationDependencies() {
|
private void initializeRegistrationDependencies() {
|
||||||
org.signal.registration.RegistrationDependencies.Companion.provide(
|
RegistrationDependencies.provide(
|
||||||
new org.signal.registration.RegistrationDependencies(
|
new RegistrationDependencies(
|
||||||
new org.thoughtcrime.securesms.registration.v2.AppRegistrationNetworkController(this, AppDependencies.getPushServiceSocket()),
|
new AppRegistrationNetworkController(this, AppDependencies.getPushServiceSocket()),
|
||||||
new org.thoughtcrime.securesms.registration.v2.AppRegistrationStorageController(this),
|
new AppRegistrationStorageController(this),
|
||||||
Environment.IS_LINK_AND_SYNC_AVAILABLE,
|
Environment.IS_LINK_AND_SYNC_AVAILABLE,
|
||||||
null,
|
null,
|
||||||
context -> {
|
context -> {
|
||||||
@ -571,7 +578,7 @@ public class ApplicationContext extends Application implements AppForegroundObse
|
|||||||
|
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
private void initializeBlobProvider() {
|
private void initializeBlobProvider() {
|
||||||
BlobProvider.getInstance().initialize(this);
|
AppDependencies.getBlobs().initialize(this, DraftBlobs.INSTANCE::deleteOrphanedDraftFiles);
|
||||||
}
|
}
|
||||||
|
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
|
|||||||
@ -1,12 +1,14 @@
|
|||||||
package org.thoughtcrime.securesms;
|
package org.thoughtcrime.securesms;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
import androidx.lifecycle.LifecycleOwner;
|
import androidx.lifecycle.LifecycleOwner;
|
||||||
|
|
||||||
import com.bumptech.glide.RequestManager;
|
import com.bumptech.glide.RequestManager;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.conversationlist.model.ConversationSet;
|
import org.thoughtcrime.securesms.conversationlist.model.ConversationSet;
|
||||||
import org.thoughtcrime.securesms.database.model.ThreadWithRecipient;
|
import org.thoughtcrime.securesms.database.model.ThreadWithRecipient;
|
||||||
|
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||||
|
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
@ -18,10 +20,10 @@ public interface BindableConversationListItem extends Unbindable {
|
|||||||
@NonNull RequestManager requestManager, @NonNull Locale locale,
|
@NonNull RequestManager requestManager, @NonNull Locale locale,
|
||||||
@NonNull Set<Long> typingThreads,
|
@NonNull Set<Long> typingThreads,
|
||||||
@NonNull ConversationSet selectedConversations,
|
@NonNull ConversationSet selectedConversations,
|
||||||
long activeThreadId);
|
@Nullable RecipientId activeRecipientId);
|
||||||
|
|
||||||
void setSelectedConversations(@NonNull ConversationSet conversations);
|
void setSelectedConversations(@NonNull ConversationSet conversations);
|
||||||
void setActiveThreadId(long activeThreadId);
|
void setActiveRecipientId(@Nullable RecipientId activeRecipientId);
|
||||||
void updateTypingIndicator(@NonNull Set<Long> typingThreads);
|
void updateTypingIndicator(@NonNull Set<Long> typingThreads);
|
||||||
void updateTimestamp();
|
void updateTimestamp();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -600,7 +600,18 @@ public final class ContactSelectionListFragment extends LoggingFragment {
|
|||||||
boolean isUnknown = contact instanceof ContactSearchKey.UnknownRecipientKey;
|
boolean isUnknown = contact instanceof ContactSearchKey.UnknownRecipientKey;
|
||||||
SelectedContact selectedContact = contact.requireSelectedContact();
|
SelectedContact selectedContact = contact.requireSelectedContact();
|
||||||
|
|
||||||
if (!canSelectSelf && !selectedContact.hasUsername() && Recipient.self().getId().equals(selectedContact.getOrCreateRecipientId())) {
|
boolean needsSelfCheck = !canSelectSelf && !selectedContact.hasUsername();
|
||||||
|
|
||||||
|
if (needsSelfCheck) {
|
||||||
|
lifecycleDisposable.add(contactChipViewModel.isSelf(selectedContact)
|
||||||
|
.subscribe(isSelf -> onItemClickResolved(contact, selectedContact, isUnknown, isSelf)));
|
||||||
|
} else {
|
||||||
|
onItemClickResolved(contact, selectedContact, isUnknown, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onItemClickResolved(ContactSearchKey contact, SelectedContact selectedContact, boolean isUnknown, boolean isSelf) {
|
||||||
|
if (isSelf) {
|
||||||
Toast.makeText(requireContext(), R.string.ContactSelectionListFragment_you_do_not_need_to_add_yourself_to_the_group, Toast.LENGTH_SHORT).show();
|
Toast.makeText(requireContext(), R.string.ContactSelectionListFragment_you_do_not_need_to_add_yourself_to_the_group, Toast.LENGTH_SHORT).show();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,7 +5,6 @@
|
|||||||
|
|
||||||
package org.thoughtcrime.securesms
|
package org.thoughtcrime.securesms
|
||||||
|
|
||||||
import android.Manifest
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
@ -15,7 +14,6 @@ import android.view.MotionEvent
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.ViewTreeObserver
|
import android.view.ViewTreeObserver
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.activity.SystemBarStyle
|
import androidx.activity.SystemBarStyle
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
@ -72,6 +70,8 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|||||||
import androidx.lifecycle.createSavedStateHandle
|
import androidx.lifecycle.createSavedStateHandle
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.lifecycle.repeatOnLifecycle
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
|
import androidx.navigation3.runtime.entryProvider
|
||||||
|
import androidx.navigation3.ui.NavDisplay
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import io.reactivex.rxjava3.subjects.PublishSubject
|
import io.reactivex.rxjava3.subjects.PublishSubject
|
||||||
@ -85,6 +85,7 @@ import kotlinx.coroutines.withContext
|
|||||||
import org.signal.core.ui.BottomSheetUtil
|
import org.signal.core.ui.BottomSheetUtil
|
||||||
import org.signal.core.ui.compose.Snackbars
|
import org.signal.core.ui.compose.Snackbars
|
||||||
import org.signal.core.ui.compose.theme.SignalTheme
|
import org.signal.core.ui.compose.theme.SignalTheme
|
||||||
|
import org.signal.core.ui.navigation.TransitionSpecs
|
||||||
import org.signal.core.ui.permissions.Permissions
|
import org.signal.core.ui.permissions.Permissions
|
||||||
import org.signal.core.ui.rememberIsSplitPane
|
import org.signal.core.ui.rememberIsSplitPane
|
||||||
import org.signal.core.util.AppForegroundObserver
|
import org.signal.core.util.AppForegroundObserver
|
||||||
@ -105,6 +106,8 @@ import org.thoughtcrime.securesms.calls.log.CallLogFragment
|
|||||||
import org.thoughtcrime.securesms.calls.new.NewCallActivity
|
import org.thoughtcrime.securesms.calls.new.NewCallActivity
|
||||||
import org.thoughtcrime.securesms.calls.quality.CallQuality
|
import org.thoughtcrime.securesms.calls.quality.CallQuality
|
||||||
import org.thoughtcrime.securesms.calls.quality.CallQualityBottomSheetFragment
|
import org.thoughtcrime.securesms.calls.quality.CallQualityBottomSheetFragment
|
||||||
|
import org.thoughtcrime.securesms.chats.ConversationTransitionState
|
||||||
|
import org.thoughtcrime.securesms.chats.chatsNavEntries
|
||||||
import org.thoughtcrime.securesms.components.DebugLogsPromptDialogFragment
|
import org.thoughtcrime.securesms.components.DebugLogsPromptDialogFragment
|
||||||
import org.thoughtcrime.securesms.components.PromptBatterySaverDialogFragment
|
import org.thoughtcrime.securesms.components.PromptBatterySaverDialogFragment
|
||||||
import org.thoughtcrime.securesms.components.compose.ConnectivityWarningBottomSheet
|
import org.thoughtcrime.securesms.components.compose.ConnectivityWarningBottomSheet
|
||||||
@ -134,7 +137,6 @@ import org.thoughtcrime.securesms.devicetransfer.olddevice.OldDeviceExitActivity
|
|||||||
import org.thoughtcrime.securesms.groups.ui.creategroup.CreateGroupActivity
|
import org.thoughtcrime.securesms.groups.ui.creategroup.CreateGroupActivity
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
import org.thoughtcrime.securesms.lock.v2.CreateSvrPinActivity
|
import org.thoughtcrime.securesms.lock.v2.CreateSvrPinActivity
|
||||||
import org.thoughtcrime.securesms.main.ChatNavGraphState
|
|
||||||
import org.thoughtcrime.securesms.main.DetailsScreenNavHost
|
import org.thoughtcrime.securesms.main.DetailsScreenNavHost
|
||||||
import org.thoughtcrime.securesms.main.MainBottomChrome
|
import org.thoughtcrime.securesms.main.MainBottomChrome
|
||||||
import org.thoughtcrime.securesms.main.MainBottomChromeCallback
|
import org.thoughtcrime.securesms.main.MainBottomChromeCallback
|
||||||
@ -143,7 +145,6 @@ import org.thoughtcrime.securesms.main.MainContentLayoutData
|
|||||||
import org.thoughtcrime.securesms.main.MainMegaphoneState
|
import org.thoughtcrime.securesms.main.MainMegaphoneState
|
||||||
import org.thoughtcrime.securesms.main.MainNavigationBar
|
import org.thoughtcrime.securesms.main.MainNavigationBar
|
||||||
import org.thoughtcrime.securesms.main.MainNavigationDetailLocation
|
import org.thoughtcrime.securesms.main.MainNavigationDetailLocation
|
||||||
import org.thoughtcrime.securesms.main.MainNavigationDetailLocationEffect
|
|
||||||
import org.thoughtcrime.securesms.main.MainNavigationListLocation
|
import org.thoughtcrime.securesms.main.MainNavigationListLocation
|
||||||
import org.thoughtcrime.securesms.main.MainNavigationRail
|
import org.thoughtcrime.securesms.main.MainNavigationRail
|
||||||
import org.thoughtcrime.securesms.main.MainNavigationRouter
|
import org.thoughtcrime.securesms.main.MainNavigationRouter
|
||||||
@ -157,12 +158,10 @@ import org.thoughtcrime.securesms.main.MainToolbarState
|
|||||||
import org.thoughtcrime.securesms.main.MainToolbarViewModel
|
import org.thoughtcrime.securesms.main.MainToolbarViewModel
|
||||||
import org.thoughtcrime.securesms.main.Material3OnScrollHelperBinder
|
import org.thoughtcrime.securesms.main.Material3OnScrollHelperBinder
|
||||||
import org.thoughtcrime.securesms.main.callNavGraphBuilder
|
import org.thoughtcrime.securesms.main.callNavGraphBuilder
|
||||||
import org.thoughtcrime.securesms.main.chatNavGraphBuilder
|
|
||||||
import org.thoughtcrime.securesms.main.navigateToDetailLocation
|
import org.thoughtcrime.securesms.main.navigateToDetailLocation
|
||||||
import org.thoughtcrime.securesms.main.rememberDetailNavHostController
|
import org.thoughtcrime.securesms.main.rememberDetailNavHostController
|
||||||
import org.thoughtcrime.securesms.main.rememberFocusRequester
|
import org.thoughtcrime.securesms.main.rememberFocusRequester
|
||||||
import org.thoughtcrime.securesms.main.storiesNavGraphBuilder
|
import org.thoughtcrime.securesms.main.storiesNavGraphBuilder
|
||||||
import org.thoughtcrime.securesms.mediasend.camerax.CameraXRemoteConfig
|
|
||||||
import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionActivity
|
import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionActivity
|
||||||
import org.thoughtcrime.securesms.mediasend.v3.mediaSendLauncher
|
import org.thoughtcrime.securesms.mediasend.v3.mediaSendLauncher
|
||||||
import org.thoughtcrime.securesms.megaphone.Megaphone
|
import org.thoughtcrime.securesms.megaphone.Megaphone
|
||||||
@ -197,7 +196,6 @@ import org.thoughtcrime.securesms.window.NavigationType
|
|||||||
import org.thoughtcrime.securesms.window.rememberThreePaneScaffoldNavigatorDelegate
|
import org.thoughtcrime.securesms.window.rememberThreePaneScaffoldNavigatorDelegate
|
||||||
import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState
|
import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState
|
||||||
import kotlin.time.Duration.Companion.minutes
|
import kotlin.time.Duration.Companion.minutes
|
||||||
import org.signal.core.ui.R as CoreUiR
|
|
||||||
|
|
||||||
class MainActivity :
|
class MainActivity :
|
||||||
PassphraseRequiredActivity(),
|
PassphraseRequiredActivity(),
|
||||||
@ -498,18 +496,15 @@ class MainActivity :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val chatNavGraphState = ChatNavGraphState.remember(isSplitPane)
|
val convoTransitionState = ConversationTransitionState.remember(isSplitPane)
|
||||||
val mutableInteractionSource = remember { MutableInteractionSource() }
|
val mutableInteractionSource = remember { MutableInteractionSource() }
|
||||||
MainNavigationDetailLocationEffect(mainNavigationViewModel, chatNavGraphState::writeGraphicsLayerToBitmap)
|
|
||||||
|
|
||||||
val chatsNavHostController = rememberDetailNavHostController(
|
LaunchedEffect(convoTransitionState) {
|
||||||
onRequestFocus = rememberFocusRequester(
|
mainNavigationViewModel.setChatListSnapshotCaptureProvider { convoTransitionState.writeGraphicsLayerToBitmap() }
|
||||||
mainNavigationViewModel = mainNavigationViewModel,
|
}
|
||||||
currentListLocation = mainNavigationState.currentListLocation,
|
|
||||||
isTargetListLocation = { it in listOf(MainNavigationListLocation.CHATS, MainNavigationListLocation.ARCHIVE) }
|
LaunchedEffect(isSplitPane) {
|
||||||
)
|
mainNavigationViewModel.onSplitPaneChanged(isSplitPane)
|
||||||
) {
|
|
||||||
chatNavGraphBuilder(chatNavGraphState)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val callsNavHostController = rememberDetailNavHostController(
|
val callsNavHostController = rememberDetailNavHostController(
|
||||||
@ -531,22 +526,23 @@ class MainActivity :
|
|||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
suspend fun navigateToLocation(location: MainNavigationDetailLocation) {
|
fun navigateToLocation(location: MainNavigationDetailLocation) {
|
||||||
when (location) {
|
when (location) {
|
||||||
is MainNavigationDetailLocation.Empty -> {
|
is MainNavigationDetailLocation.Empty -> {
|
||||||
when (mainNavigationState.currentListLocation) {
|
when (mainNavigationState.currentListLocation) {
|
||||||
MainNavigationListLocation.CHATS, MainNavigationListLocation.ARCHIVE -> chatsNavHostController
|
MainNavigationListLocation.CHATS, MainNavigationListLocation.ARCHIVE -> {
|
||||||
|
throw IllegalStateException("Navigation to ${mainNavigationState.currentListLocation} should be handled by ChatsBackStack.")
|
||||||
|
}
|
||||||
|
|
||||||
MainNavigationListLocation.CALLS -> callsNavHostController
|
MainNavigationListLocation.CALLS -> callsNavHostController
|
||||||
MainNavigationListLocation.STORIES -> storiesNavHostController
|
MainNavigationListLocation.STORIES -> storiesNavHostController
|
||||||
}.navigateToDetailLocation(location)
|
}.navigateToDetailLocation(location)
|
||||||
}
|
}
|
||||||
|
|
||||||
is MainNavigationDetailLocation.Conversation -> {
|
is MainNavigationDetailLocation.Conversation, is MainNavigationDetailLocation.Chats -> {
|
||||||
chatNavGraphState.writeGraphicsLayerToBitmap()
|
throw IllegalStateException("Navigation to $location should be handled by ChatsBackStack.")
|
||||||
chatsNavHostController.navigateToDetailLocation(location)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
is MainNavigationDetailLocation.Chats -> chatsNavHostController.navigateToDetailLocation(location)
|
|
||||||
is MainNavigationDetailLocation.CallLinkDetails -> callsNavHostController.navigateToDetailLocation(location)
|
is MainNavigationDetailLocation.CallLinkDetails -> callsNavHostController.navigateToDetailLocation(location)
|
||||||
is MainNavigationDetailLocation.Calls -> callsNavHostController.navigateToDetailLocation(location)
|
is MainNavigationDetailLocation.Calls -> callsNavHostController.navigateToDetailLocation(location)
|
||||||
is MainNavigationDetailLocation.Stories -> storiesNavHostController.navigateToDetailLocation(location)
|
is MainNavigationDetailLocation.Stories -> storiesNavHostController.navigateToDetailLocation(location)
|
||||||
@ -560,7 +556,9 @@ class MainActivity :
|
|||||||
}
|
}
|
||||||
|
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
BackHandler(paneExpansionState.currentAnchor == detailOnlyAnchor) {
|
BackHandler(paneExpansionState.currentAnchor == detailOnlyAnchor) {
|
||||||
|
mainNavigationViewModel.goTo(MainNavigationDetailLocation.Empty)
|
||||||
scope.launch {
|
scope.launch {
|
||||||
paneExpansionState.animateTo(listOnlyAnchor)
|
paneExpansionState.animateTo(listOnlyAnchor)
|
||||||
}
|
}
|
||||||
@ -619,7 +617,7 @@ class MainActivity :
|
|||||||
|
|
||||||
AppScaffold(
|
AppScaffold(
|
||||||
navigator = wrappedNavigator,
|
navigator = wrappedNavigator,
|
||||||
modifier = chatNavGraphState.writeContentToGraphicsLayer(),
|
modifier = convoTransitionState.writeContentToGraphicsLayer(),
|
||||||
paneExpansionState = paneExpansionState,
|
paneExpansionState = paneExpansionState,
|
||||||
contentWindowInsets = WindowInsets(),
|
contentWindowInsets = WindowInsets(),
|
||||||
snackbarHost = {
|
snackbarHost = {
|
||||||
@ -730,9 +728,13 @@ class MainActivity :
|
|||||||
primaryContent = {
|
primaryContent = {
|
||||||
when (mainNavigationState.currentListLocation) {
|
when (mainNavigationState.currentListLocation) {
|
||||||
MainNavigationListLocation.CHATS, MainNavigationListLocation.ARCHIVE -> {
|
MainNavigationListLocation.CHATS, MainNavigationListLocation.ARCHIVE -> {
|
||||||
DetailsScreenNavHost(
|
NavDisplay(
|
||||||
navHostController = chatsNavHostController,
|
backStack = mainNavigationViewModel.chatsBackStackEntries,
|
||||||
contentLayoutData = contentLayoutData
|
onBack = { mainNavigationViewModel.popChatsDetailLocation() },
|
||||||
|
transitionSpec = TransitionSpecs.HorizontalSlide.transitionSpec,
|
||||||
|
popTransitionSpec = TransitionSpecs.HorizontalSlide.popTransitionSpec,
|
||||||
|
predictivePopTransitionSpec = TransitionSpecs.HorizontalSlide.predictivePopTransitionSpec,
|
||||||
|
entryProvider = entryProvider { chatsNavEntries(convoTransitionState) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -761,7 +763,7 @@ class MainActivity :
|
|||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
},
|
},
|
||||||
animatorFactory = if (mainNavigationState.currentListLocation == MainNavigationListLocation.CHATS || mainNavigationState.currentListLocation == MainNavigationListLocation.ARCHIVE) {
|
animatorFactory = if (mainNavigationState.currentListLocation.isChatsTab) {
|
||||||
noEnterTransitionFactory
|
noEnterTransitionFactory
|
||||||
} else {
|
} else {
|
||||||
AppScaffoldAnimationStateFactory.Default
|
AppScaffoldAnimationStateFactory.Default
|
||||||
@ -1175,24 +1177,7 @@ class MainActivity :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (CameraXRemoteConfig.isSupported()) {
|
onGranted()
|
||||||
onGranted()
|
|
||||||
} else {
|
|
||||||
Permissions.with(this@MainActivity)
|
|
||||||
.request(Manifest.permission.CAMERA)
|
|
||||||
.ifNecessary()
|
|
||||||
.withRationaleDialog(getString(R.string.CameraXFragment_allow_access_camera), getString(R.string.CameraXFragment_to_capture_photos_and_video_allow_camera), CoreUiR.drawable.symbol_camera_24)
|
|
||||||
.withPermanentDenialDialog(
|
|
||||||
getString(R.string.CameraXFragment_signal_needs_camera_access_capture_photos),
|
|
||||||
null,
|
|
||||||
R.string.CameraXFragment_allow_access_camera,
|
|
||||||
R.string.CameraXFragment_to_capture_photos_videos,
|
|
||||||
supportFragmentManager
|
|
||||||
)
|
|
||||||
.onAllGranted(onGranted)
|
|
||||||
.onAnyDenied { Toast.makeText(this@MainActivity, R.string.CameraXFragment_signal_needs_camera_access_capture_photos, Toast.LENGTH_LONG).show() }
|
|
||||||
.execute()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class ToolbarCallback : MainToolbarCallback {
|
inner class ToolbarCallback : MainToolbarCallback {
|
||||||
|
|||||||
@ -135,12 +135,8 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
|
|||||||
Intent intent = getIntentForState(applicationState);
|
Intent intent = getIntentForState(applicationState);
|
||||||
if (intent != null) {
|
if (intent != null) {
|
||||||
Log.d(TAG, "routeApplicationState(), intent: " + intent.getComponent());
|
Log.d(TAG, "routeApplicationState(), intent: " + intent.getComponent());
|
||||||
if (applicationState == STATE_WELCOME_PUSH_SCREEN && Environment.USE_NEW_REGISTRATION) {
|
|
||||||
startActivity(intent);
|
|
||||||
} else {
|
|
||||||
startActivity(intent);
|
startActivity(intent);
|
||||||
finish();
|
finish();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -227,7 +223,7 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
|
|||||||
|
|
||||||
private Intent getPushRegistrationIntent() {
|
private Intent getPushRegistrationIntent() {
|
||||||
if (Environment.USE_NEW_REGISTRATION) {
|
if (Environment.USE_NEW_REGISTRATION) {
|
||||||
return org.signal.registration.RegistrationActivity.createIntent(this);
|
return org.signal.registration.RegistrationActivity.createIntent(this, MainActivity.clearTop(this));
|
||||||
} else {
|
} else {
|
||||||
return RegistrationActivity.newIntentForNewRegistration(this, getIntent());
|
return RegistrationActivity.newIntentForNewRegistration(this, getIntent());
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,107 +1,27 @@
|
|||||||
package org.thoughtcrime.securesms;
|
package org.thoughtcrime.securesms;
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.provider.ContactsContract;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.activity.ComponentActivity;
|
||||||
|
import androidx.lifecycle.ViewModelProvider;
|
||||||
|
|
||||||
import org.signal.core.util.logging.Log;
|
public class SystemContactsEntrypointActivity extends ComponentActivity {
|
||||||
import org.thoughtcrime.securesms.conversation.ConversationIntents;
|
|
||||||
import org.thoughtcrime.securesms.conversation.NewConversationActivity;
|
|
||||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
|
||||||
import org.thoughtcrime.securesms.util.Rfc5724Uri;
|
|
||||||
|
|
||||||
import java.net.URISyntaxException;
|
|
||||||
|
|
||||||
public class SystemContactsEntrypointActivity extends Activity {
|
|
||||||
|
|
||||||
private static final String TAG = Log.tag(SystemContactsEntrypointActivity.class);
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
startActivity(getNextIntent(getIntent()));
|
|
||||||
finish();
|
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
}
|
|
||||||
|
|
||||||
private Intent getNextIntent(Intent original) {
|
SystemContactsEntrypointViewModel viewModel = new ViewModelProvider(this).get(SystemContactsEntrypointViewModel.class);
|
||||||
DestinationAndBody destination;
|
|
||||||
|
|
||||||
if (original.getData() != null && "content".equals(original.getData().getScheme())) {
|
viewModel.getContactAction().observe(this, nextStep -> {
|
||||||
destination = getDestinationForSyncAdapter(original);
|
if (nextStep.getShowSpecifyRecipientToast()) {
|
||||||
} else {
|
|
||||||
destination = getDestinationForView(original);
|
|
||||||
}
|
|
||||||
|
|
||||||
final Intent nextIntent;
|
|
||||||
|
|
||||||
if (TextUtils.isEmpty(destination.destination)) {
|
|
||||||
nextIntent = NewConversationActivity.createIntent(this, destination.getBody());
|
|
||||||
Toast.makeText(this, R.string.ConversationActivity_specify_recipient, Toast.LENGTH_LONG).show();
|
|
||||||
} else {
|
|
||||||
Recipient recipient = Recipient.external(destination.getDestination());
|
|
||||||
|
|
||||||
if (recipient != null) {
|
|
||||||
long threadId = SignalDatabase.threads().getOrCreateThreadIdFor(recipient);
|
|
||||||
|
|
||||||
nextIntent = ConversationIntents.createBuilderSync(this, recipient.getId(), threadId)
|
|
||||||
.withDraftText(destination.getBody())
|
|
||||||
.build();
|
|
||||||
} else {
|
|
||||||
nextIntent = NewConversationActivity.createIntent(this, destination.getBody());
|
|
||||||
Toast.makeText(this, R.string.ConversationActivity_specify_recipient, Toast.LENGTH_LONG).show();
|
Toast.makeText(this, R.string.ConversationActivity_specify_recipient, Toast.LENGTH_LONG).show();
|
||||||
}
|
}
|
||||||
}
|
startActivity(nextStep.getIntent());
|
||||||
return nextIntent;
|
finish();
|
||||||
}
|
});
|
||||||
|
|
||||||
private @NonNull DestinationAndBody getDestinationForView(Intent intent) {
|
viewModel.resolveNextStep(getIntent());
|
||||||
try {
|
|
||||||
Rfc5724Uri smsUri = new Rfc5724Uri(intent.getData().toString());
|
|
||||||
return new DestinationAndBody(smsUri.getPath(), smsUri.getQueryParams().get("body"));
|
|
||||||
} catch (URISyntaxException e) {
|
|
||||||
Log.w(TAG, "unable to parse RFC5724 URI from intent", e);
|
|
||||||
return new DestinationAndBody("", "");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private @NonNull DestinationAndBody getDestinationForSyncAdapter(Intent intent) {
|
|
||||||
Cursor cursor = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
cursor = getContentResolver().query(intent.getData(), null, null, null, null);
|
|
||||||
|
|
||||||
if (cursor != null && cursor.moveToNext()) {
|
|
||||||
return new DestinationAndBody(cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.RawContacts.Data.DATA1)), "");
|
|
||||||
}
|
|
||||||
|
|
||||||
return new DestinationAndBody("", "");
|
|
||||||
} finally {
|
|
||||||
if (cursor != null) cursor.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class DestinationAndBody {
|
|
||||||
private final String destination;
|
|
||||||
private final String body;
|
|
||||||
|
|
||||||
private DestinationAndBody(String destination, String body) {
|
|
||||||
this.destination = destination;
|
|
||||||
this.body = body;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getDestination() {
|
|
||||||
return destination;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getBody() {
|
|
||||||
return body;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,100 @@
|
|||||||
|
package org.thoughtcrime.securesms
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.provider.ContactsContract
|
||||||
|
import android.text.TextUtils
|
||||||
|
import androidx.annotation.WorkerThread
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.signal.core.util.logging.Log
|
||||||
|
import org.thoughtcrime.securesms.conversation.ConversationIntents
|
||||||
|
import org.thoughtcrime.securesms.conversation.NewConversationActivity
|
||||||
|
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||||
|
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||||
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
|
import org.thoughtcrime.securesms.util.Rfc5724Uri
|
||||||
|
import java.net.URISyntaxException
|
||||||
|
|
||||||
|
class SystemContactsEntrypointViewModel : ViewModel() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = Log.tag(SystemContactsEntrypointViewModel::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val internalContactAction = MutableLiveData<ContactAction>()
|
||||||
|
val contactAction: LiveData<ContactAction> = internalContactAction
|
||||||
|
|
||||||
|
fun resolveNextStep(original: Intent) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val result = withContext(Dispatchers.IO) {
|
||||||
|
getContactAction(AppDependencies.application, original)
|
||||||
|
}
|
||||||
|
|
||||||
|
internalContactAction.value = result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
private fun getContactAction(context: Context, original: Intent): ContactAction {
|
||||||
|
val destination = if (original.data != null && "content" == original.data?.scheme) {
|
||||||
|
getDestinationForSyncAdapter(context, original)
|
||||||
|
} else {
|
||||||
|
getDestinationForView(original)
|
||||||
|
}
|
||||||
|
|
||||||
|
val destinationAddress = destination.destination
|
||||||
|
if (TextUtils.isEmpty(destinationAddress)) {
|
||||||
|
return ContactAction(NewConversationActivity.createIntent(context, destination.body), true)
|
||||||
|
}
|
||||||
|
|
||||||
|
val recipient = Recipient.external(destinationAddress!!)
|
||||||
|
|
||||||
|
if (recipient != null) {
|
||||||
|
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(recipient)
|
||||||
|
|
||||||
|
val nextIntent = ConversationIntents.createBuilderSync(context, recipient.id, threadId)
|
||||||
|
.withDraftText(destination.body)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
return ContactAction(nextIntent, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ContactAction(NewConversationActivity.createIntent(context, destination.body), true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getDestinationForView(intent: Intent): DestinationAndBody {
|
||||||
|
return try {
|
||||||
|
val smsUri = Rfc5724Uri(intent.data.toString())
|
||||||
|
DestinationAndBody(smsUri.path, smsUri.queryParams["body"])
|
||||||
|
} catch (e: URISyntaxException) {
|
||||||
|
Log.w(TAG, "unable to parse RFC5724 URI from intent", e)
|
||||||
|
DestinationAndBody("", "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getDestinationForSyncAdapter(context: Context, intent: Intent): DestinationAndBody {
|
||||||
|
context.contentResolver.query(intent.data!!, null, null, null, null).use { cursor ->
|
||||||
|
if (cursor != null && cursor.moveToNext()) {
|
||||||
|
return DestinationAndBody(cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.RawContacts.Data.DATA1)), "")
|
||||||
|
}
|
||||||
|
|
||||||
|
return DestinationAndBody("", "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class ContactAction(
|
||||||
|
val intent: Intent,
|
||||||
|
val showSpecifyRecipientToast: Boolean
|
||||||
|
)
|
||||||
|
|
||||||
|
private data class DestinationAndBody(
|
||||||
|
val destination: String?,
|
||||||
|
val body: String?
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -4,6 +4,7 @@ import android.net.Uri
|
|||||||
import android.os.Parcel
|
import android.os.Parcel
|
||||||
import androidx.core.os.ParcelCompat
|
import androidx.core.os.ParcelCompat
|
||||||
import org.signal.blurhash.BlurHash
|
import org.signal.blurhash.BlurHash
|
||||||
|
import org.signal.core.models.database.AttachmentId
|
||||||
import org.signal.core.models.media.TransformProperties
|
import org.signal.core.models.media.TransformProperties
|
||||||
import org.signal.core.util.ParcelUtil
|
import org.signal.core.util.ParcelUtil
|
||||||
import org.thoughtcrime.securesms.audio.AudioHash
|
import org.thoughtcrime.securesms.audio.AudioHash
|
||||||
|
|||||||
@ -11,10 +11,11 @@ import androidx.annotation.Nullable;
|
|||||||
|
|
||||||
import org.signal.core.util.ThreadUtil;
|
import org.signal.core.util.ThreadUtil;
|
||||||
import org.signal.core.util.concurrent.SignalExecutors;
|
import org.signal.core.util.concurrent.SignalExecutors;
|
||||||
|
import org.signal.core.util.contentproviders.BlobProvider;
|
||||||
import org.signal.core.util.logging.Log;
|
import org.signal.core.util.logging.Log;
|
||||||
import org.thoughtcrime.securesms.components.voice.VoiceNoteDraft;
|
import org.thoughtcrime.securesms.components.voice.VoiceNoteDraft;
|
||||||
|
import org.thoughtcrime.securesms.dependencies.AppDependencies;
|
||||||
import org.thoughtcrime.securesms.notifications.v2.InChatNotificationSoundSuppressor;
|
import org.thoughtcrime.securesms.notifications.v2.InChatNotificationSoundSuppressor;
|
||||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
|
||||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@ -88,9 +89,9 @@ public class AudioRecorder {
|
|||||||
|
|
||||||
ParcelFileDescriptor fds[] = ParcelFileDescriptor.createPipe();
|
ParcelFileDescriptor fds[] = ParcelFileDescriptor.createPipe();
|
||||||
|
|
||||||
BlobProvider.BlobBuilder blobBuilder = BlobProvider.getInstance()
|
BlobProvider.BlobBuilder blobBuilder = AppDependencies.getBlobs()
|
||||||
.forData(new ParcelFileDescriptor.AutoCloseInputStream(fds[0]), 0)
|
.forData(new ParcelFileDescriptor.AutoCloseInputStream(fds[0]), 0)
|
||||||
.withMimeType(MediaUtil.AUDIO_AAC);
|
.withMimeType(MediaUtil.AUDIO_AAC);
|
||||||
|
|
||||||
recordingUri = blobBuilder.buildUriForDraftAttachment();
|
recordingUri = blobBuilder.buildUriForDraftAttachment();
|
||||||
recordingUriFuture = blobBuilder.createForDraftAttachmentAsync(context);
|
recordingUriFuture = blobBuilder.createForDraftAttachmentAsync(context);
|
||||||
|
|||||||
@ -33,7 +33,7 @@ public final class AudioWaveFormGenerator {
|
|||||||
*/
|
*/
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
public static @NonNull AudioFileInfo generateWaveForm(@NonNull Context context, @NonNull Uri uri) throws IOException {
|
public static @NonNull AudioFileInfo generateWaveForm(@NonNull Context context, @NonNull Uri uri) throws IOException {
|
||||||
try (MediaInput dataSource = DecryptableUriMediaInput.createForUri(context, uri)) {
|
try (MediaInput dataSource = DecryptableUriMediaInput.INSTANCE.createForUri(context, uri)) {
|
||||||
long[] wave = new long[BAR_COUNT];
|
long[] wave = new long[BAR_COUNT];
|
||||||
int[] waveSamples = new int[BAR_COUNT];
|
int[] waveSamples = new int[BAR_COUNT];
|
||||||
|
|
||||||
|
|||||||
@ -7,9 +7,9 @@ import androidx.annotation.AnyThread
|
|||||||
import io.reactivex.rxjava3.core.Single
|
import io.reactivex.rxjava3.core.Single
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||||
import io.reactivex.rxjava3.subjects.SingleSubject
|
import io.reactivex.rxjava3.subjects.SingleSubject
|
||||||
|
import org.signal.core.models.database.AttachmentId
|
||||||
import org.signal.core.util.logging.Log
|
import org.signal.core.util.logging.Log
|
||||||
import org.thoughtcrime.securesms.attachments.Attachment
|
import org.thoughtcrime.securesms.attachments.Attachment
|
||||||
import org.thoughtcrime.securesms.attachments.AttachmentId
|
|
||||||
import org.thoughtcrime.securesms.attachments.DatabaseAttachment
|
import org.thoughtcrime.securesms.attachments.DatabaseAttachment
|
||||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||||
import org.thoughtcrime.securesms.database.model.databaseprotos.AudioWaveFormData
|
import org.thoughtcrime.securesms.database.model.databaseprotos.AudioWaveFormData
|
||||||
|
|||||||
@ -11,9 +11,9 @@ import androidx.appcompat.content.res.AppCompatResources
|
|||||||
import com.airbnb.lottie.SimpleColorFilter
|
import com.airbnb.lottie.SimpleColorFilter
|
||||||
import org.signal.core.models.media.Media
|
import org.signal.core.models.media.Media
|
||||||
import org.signal.core.util.concurrent.SignalExecutors
|
import org.signal.core.util.concurrent.SignalExecutors
|
||||||
|
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||||
import org.thoughtcrime.securesms.mms.PartAuthority
|
import org.thoughtcrime.securesms.mms.PartAuthority
|
||||||
import org.thoughtcrime.securesms.profiles.AvatarHelper
|
import org.thoughtcrime.securesms.profiles.AvatarHelper
|
||||||
import org.thoughtcrime.securesms.providers.BlobProvider
|
|
||||||
import org.thoughtcrime.securesms.util.MediaUtil
|
import org.thoughtcrime.securesms.util.MediaUtil
|
||||||
import java.io.ByteArrayInputStream
|
import java.io.ByteArrayInputStream
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
@ -80,7 +80,7 @@ object AvatarRenderer {
|
|||||||
|
|
||||||
private fun renderPhoto(context: Context, avatar: Avatar.Photo, onAvatarRendered: (Media) -> Unit) {
|
private fun renderPhoto(context: Context, avatar: Avatar.Photo, onAvatarRendered: (Media) -> Unit) {
|
||||||
SignalExecutors.BOUNDED.execute {
|
SignalExecutors.BOUNDED.execute {
|
||||||
val blob = BlobProvider.getInstance()
|
val blob = AppDependencies.blobs
|
||||||
.forData(AvatarPickerStorage.read(context, PartAuthority.getAvatarPickerFilename(avatar.uri)), avatar.size)
|
.forData(AvatarPickerStorage.read(context, PartAuthority.getAvatarPickerFilename(avatar.uri)), avatar.size)
|
||||||
.createForSingleSessionOnDisk(context)
|
.createForSingleSessionOnDisk(context)
|
||||||
|
|
||||||
@ -124,7 +124,7 @@ object AvatarRenderer {
|
|||||||
|
|
||||||
val bytes = outStream.toByteArray()
|
val bytes = outStream.toByteArray()
|
||||||
val inStream = ByteArrayInputStream(bytes)
|
val inStream = ByteArrayInputStream(bytes)
|
||||||
val uri = BlobProvider.getInstance().forData(inStream, bytes.size.toLong()).createForSingleSessionOnDisk(context)
|
val uri = AppDependencies.blobs.forData(inStream, bytes.size.toLong()).createForSingleSessionOnDisk(context)
|
||||||
|
|
||||||
onAvatarRendered(createMedia(uri, bytes.size.toLong()))
|
onAvatarRendered(createMedia(uri, bytes.size.toLong()))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import org.thoughtcrime.securesms.R
|
|||||||
import org.thoughtcrime.securesms.avatar.AvatarBundler
|
import org.thoughtcrime.securesms.avatar.AvatarBundler
|
||||||
import org.thoughtcrime.securesms.avatar.AvatarPickerStorage
|
import org.thoughtcrime.securesms.avatar.AvatarPickerStorage
|
||||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||||
import org.thoughtcrime.securesms.providers.BlobProvider
|
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||||
import org.thoughtcrime.securesms.scribbles.ImageEditorFragment
|
import org.thoughtcrime.securesms.scribbles.ImageEditorFragment
|
||||||
|
|
||||||
class PhotoEditorFragment : Fragment(R.layout.avatar_photo_editor_fragment), ImageEditorFragment.Controller {
|
class PhotoEditorFragment : Fragment(R.layout.avatar_photo_editor_fragment), ImageEditorFragment.Controller {
|
||||||
@ -39,15 +39,15 @@ class PhotoEditorFragment : Fragment(R.layout.avatar_photo_editor_fragment), Ima
|
|||||||
|
|
||||||
SignalExecutors.BOUNDED.execute {
|
SignalExecutors.BOUNDED.execute {
|
||||||
val editedImageUri = imageEditorFragment.renderToSingleUseBlob()
|
val editedImageUri = imageEditorFragment.renderToSingleUseBlob()
|
||||||
val size = BlobProvider.getFileSize(editedImageUri) ?: 0
|
val size = AppDependencies.blobs.getFileSize(editedImageUri) ?: 0
|
||||||
val inputStream = BlobProvider.getInstance().getStream(applicationContext, editedImageUri)
|
val inputStream = AppDependencies.blobs.getStream(applicationContext, editedImageUri)
|
||||||
val onDiskUri = AvatarPickerStorage.save(applicationContext, inputStream)
|
val onDiskUri = AvatarPickerStorage.save(applicationContext, inputStream)
|
||||||
val photo = AvatarBundler.extractPhoto(args.photoAvatar)
|
val photo = AvatarBundler.extractPhoto(args.photoAvatar)
|
||||||
val database = SignalDatabase.avatarPicker
|
val database = SignalDatabase.avatarPicker
|
||||||
val newPhoto = photo.copy(uri = onDiskUri, size = size)
|
val newPhoto = photo.copy(uri = onDiskUri, size = size)
|
||||||
|
|
||||||
database.update(newPhoto)
|
database.update(newPhoto)
|
||||||
BlobProvider.getInstance().delete(requireContext(), photo.uri)
|
AppDependencies.blobs.delete(requireContext(), photo.uri)
|
||||||
|
|
||||||
ThreadUtil.runOnMain {
|
ThreadUtil.runOnMain {
|
||||||
setFragmentResult(REQUEST_KEY_EDIT, AvatarBundler.bundlePhoto(newPhoto))
|
setFragmentResult(REQUEST_KEY_EDIT, AvatarBundler.bundlePhoto(newPhoto))
|
||||||
|
|||||||
@ -1,13 +1,11 @@
|
|||||||
package org.thoughtcrime.securesms.avatar.picker
|
package org.thoughtcrime.securesms.avatar.picker
|
||||||
|
|
||||||
import android.Manifest
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.Gravity
|
import android.view.Gravity
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.PopupMenu
|
import android.widget.PopupMenu
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.activity.result.ActivityResultLauncher
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
@ -30,12 +28,10 @@ import org.thoughtcrime.securesms.avatar.vector.VectorAvatarCreationFragment
|
|||||||
import org.thoughtcrime.securesms.components.ButtonStripItemView
|
import org.thoughtcrime.securesms.components.ButtonStripItemView
|
||||||
import org.thoughtcrime.securesms.components.recyclerview.GridDividerDecoration
|
import org.thoughtcrime.securesms.components.recyclerview.GridDividerDecoration
|
||||||
import org.thoughtcrime.securesms.mediasend.AvatarSelectionActivity
|
import org.thoughtcrime.securesms.mediasend.AvatarSelectionActivity
|
||||||
import org.thoughtcrime.securesms.mediasend.camerax.CameraXRemoteConfig
|
|
||||||
import org.thoughtcrime.securesms.util.ViewUtil
|
import org.thoughtcrime.securesms.util.ViewUtil
|
||||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||||
import org.thoughtcrime.securesms.util.visible
|
import org.thoughtcrime.securesms.util.visible
|
||||||
import org.signal.core.ui.R as CoreUiR
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Primary Avatar picker fragment, displays current user avatar and a list of recently used avatars and defaults.
|
* Primary Avatar picker fragment, displays current user avatar and a list of recently used avatars and defaults.
|
||||||
@ -223,22 +219,8 @@ class AvatarPickerFragment : Fragment(R.layout.avatar_picker_fragment) {
|
|||||||
|
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
private fun openCameraCapture() {
|
private fun openCameraCapture() {
|
||||||
if (CameraXRemoteConfig.isSupported()) {
|
val intent = AvatarSelectionActivity.getIntentForCameraCapture(requireContext())
|
||||||
val intent = AvatarSelectionActivity.getIntentForCameraCapture(requireContext())
|
startActivityForResult(intent, REQUEST_CODE_SELECT_IMAGE)
|
||||||
startActivityForResult(intent, REQUEST_CODE_SELECT_IMAGE)
|
|
||||||
} else {
|
|
||||||
Permissions.with(this)
|
|
||||||
.request(Manifest.permission.CAMERA)
|
|
||||||
.ifNecessary()
|
|
||||||
.onAllGranted {
|
|
||||||
val intent = AvatarSelectionActivity.getIntentForCameraCapture(requireContext())
|
|
||||||
startActivityForResult(intent, REQUEST_CODE_SELECT_IMAGE)
|
|
||||||
}
|
|
||||||
.withRationaleDialog(getString(R.string.CameraXFragment_allow_access_camera), getString(R.string.CameraXFragment_to_capture_photos_allow_camera), CoreUiR.drawable.symbol_camera_24)
|
|
||||||
.withPermanentDenialDialog(getString(R.string.AvatarSelectionBottomSheetDialogFragment__taking_a_photo_requires_the_camera_permission), null, R.string.CameraXFragment_allow_access_camera, R.string.CameraXFragment_to_capture_photos, getParentFragmentManager())
|
|
||||||
.onAnyDenied { Toast.makeText(requireContext(), R.string.AvatarSelectionBottomSheetDialogFragment__taking_a_photo_requires_the_camera_permission, Toast.LENGTH_SHORT).show() }
|
|
||||||
.execute()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
|
|||||||
@ -16,9 +16,9 @@ import org.thoughtcrime.securesms.avatar.AvatarRenderer
|
|||||||
import org.thoughtcrime.securesms.avatar.Avatars
|
import org.thoughtcrime.securesms.avatar.Avatars
|
||||||
import org.thoughtcrime.securesms.conversation.colors.AvatarColor
|
import org.thoughtcrime.securesms.conversation.colors.AvatarColor
|
||||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||||
|
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||||
import org.thoughtcrime.securesms.groups.GroupId
|
import org.thoughtcrime.securesms.groups.GroupId
|
||||||
import org.thoughtcrime.securesms.profiles.AvatarHelper
|
import org.thoughtcrime.securesms.profiles.AvatarHelper
|
||||||
import org.thoughtcrime.securesms.providers.BlobProvider
|
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.util.NameUtil
|
import org.thoughtcrime.securesms.util.NameUtil
|
||||||
import org.whispersystems.signalservice.api.util.StreamDetails
|
import org.whispersystems.signalservice.api.util.StreamDetails
|
||||||
@ -36,7 +36,7 @@ class AvatarPickerRepository(context: Context) {
|
|||||||
try {
|
try {
|
||||||
val bytes = StreamUtil.readFully(details.stream)
|
val bytes = StreamUtil.readFully(details.stream)
|
||||||
Avatar.Photo(
|
Avatar.Photo(
|
||||||
BlobProvider.getInstance().forData(bytes).createForSingleSessionInMemory(),
|
AppDependencies.blobs.forData(bytes).createForSingleSessionInMemory(),
|
||||||
details.length,
|
details.length,
|
||||||
Avatar.DatabaseId.DoNotPersist
|
Avatar.DatabaseId.DoNotPersist
|
||||||
)
|
)
|
||||||
@ -56,7 +56,7 @@ class AvatarPickerRepository(context: Context) {
|
|||||||
try {
|
try {
|
||||||
val bytes = AvatarHelper.getAvatarBytes(applicationContext, recipient.id)
|
val bytes = AvatarHelper.getAvatarBytes(applicationContext, recipient.id)
|
||||||
Avatar.Photo(
|
Avatar.Photo(
|
||||||
BlobProvider.getInstance().forData(bytes).createForSingleSessionInMemory(),
|
AppDependencies.blobs.forData(bytes).createForSingleSessionInMemory(),
|
||||||
AvatarHelper.getAvatarLength(applicationContext, recipient.id),
|
AvatarHelper.getAvatarLength(applicationContext, recipient.id),
|
||||||
Avatar.DatabaseId.DoNotPersist
|
Avatar.DatabaseId.DoNotPersist
|
||||||
)
|
)
|
||||||
|
|||||||
@ -19,11 +19,11 @@ import kotlinx.coroutines.flow.onEach
|
|||||||
import kotlinx.coroutines.flow.onStart
|
import kotlinx.coroutines.flow.onStart
|
||||||
import kotlinx.coroutines.flow.shareIn
|
import kotlinx.coroutines.flow.shareIn
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.signal.core.models.database.AttachmentId
|
||||||
import org.signal.core.util.bytes
|
import org.signal.core.util.bytes
|
||||||
import org.signal.core.util.logging.Log
|
import org.signal.core.util.logging.Log
|
||||||
import org.signal.core.util.throttleLatest
|
import org.signal.core.util.throttleLatest
|
||||||
import org.thoughtcrime.securesms.BuildConfig
|
import org.thoughtcrime.securesms.BuildConfig
|
||||||
import org.thoughtcrime.securesms.attachments.AttachmentId
|
|
||||||
import org.thoughtcrime.securesms.backup.v2.BackupRepository
|
import org.thoughtcrime.securesms.backup.v2.BackupRepository
|
||||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import org.signal.core.util.Conversions;
|
|||||||
import org.signal.core.util.logging.Log;
|
import org.signal.core.util.logging.Log;
|
||||||
import org.signal.libsignal.protocol.kdf.HKDF;
|
import org.signal.libsignal.protocol.kdf.HKDF;
|
||||||
import org.signal.libsignal.protocol.util.ByteUtil;
|
import org.signal.libsignal.protocol.util.ByteUtil;
|
||||||
import org.thoughtcrime.securesms.attachments.AttachmentId;
|
import org.signal.core.models.database.AttachmentId;
|
||||||
import org.thoughtcrime.securesms.backup.proto.Attachment;
|
import org.thoughtcrime.securesms.backup.proto.Attachment;
|
||||||
import org.thoughtcrime.securesms.backup.proto.Avatar;
|
import org.thoughtcrime.securesms.backup.proto.Avatar;
|
||||||
import org.thoughtcrime.securesms.backup.proto.BackupFrame;
|
import org.thoughtcrime.securesms.backup.proto.BackupFrame;
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import androidx.annotation.NonNull;
|
|||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import org.signal.core.util.logging.Log;
|
import org.signal.core.util.logging.Log;
|
||||||
import org.thoughtcrime.securesms.crypto.KeyStoreHelper;
|
import org.signal.core.util.crypto.KeyStoreHelper;
|
||||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -19,13 +19,13 @@ import org.signal.core.util.SetUtil;
|
|||||||
import org.signal.core.util.SqlUtil;
|
import org.signal.core.util.SqlUtil;
|
||||||
import org.signal.core.util.Stopwatch;
|
import org.signal.core.util.Stopwatch;
|
||||||
import org.signal.core.util.logging.Log;
|
import org.signal.core.util.logging.Log;
|
||||||
import org.thoughtcrime.securesms.attachments.AttachmentId;
|
import org.signal.core.models.database.AttachmentId;
|
||||||
import org.thoughtcrime.securesms.backup.proto.KeyValue;
|
import org.thoughtcrime.securesms.backup.proto.KeyValue;
|
||||||
import org.thoughtcrime.securesms.backup.proto.SharedPreference;
|
import org.thoughtcrime.securesms.backup.proto.SharedPreference;
|
||||||
import org.thoughtcrime.securesms.backup.proto.SqlStatement;
|
import org.thoughtcrime.securesms.backup.proto.SqlStatement;
|
||||||
import org.thoughtcrime.securesms.crypto.AttachmentSecret;
|
import org.signal.core.util.crypto.AttachmentSecret;
|
||||||
import org.thoughtcrime.securesms.crypto.ClassicDecryptingPartInputStream;
|
import org.signal.core.util.crypto.ClassicDecryptingPartInputStream;
|
||||||
import org.thoughtcrime.securesms.crypto.ModernDecryptingPartInputStream;
|
import org.signal.core.util.crypto.ModernDecryptingPartInputStream;
|
||||||
import org.thoughtcrime.securesms.database.AttachmentTable;
|
import org.thoughtcrime.securesms.database.AttachmentTable;
|
||||||
import org.thoughtcrime.securesms.database.BackupMediaSnapshotTable;
|
import org.thoughtcrime.securesms.database.BackupMediaSnapshotTable;
|
||||||
import org.thoughtcrime.securesms.database.EmojiSearchTable;
|
import org.thoughtcrime.securesms.database.EmojiSearchTable;
|
||||||
|
|||||||
@ -24,8 +24,8 @@ import org.thoughtcrime.securesms.backup.proto.KeyValue;
|
|||||||
import org.thoughtcrime.securesms.backup.proto.SharedPreference;
|
import org.thoughtcrime.securesms.backup.proto.SharedPreference;
|
||||||
import org.thoughtcrime.securesms.backup.proto.SqlStatement;
|
import org.thoughtcrime.securesms.backup.proto.SqlStatement;
|
||||||
import org.thoughtcrime.securesms.backup.proto.Sticker;
|
import org.thoughtcrime.securesms.backup.proto.Sticker;
|
||||||
import org.thoughtcrime.securesms.crypto.AttachmentSecret;
|
import org.signal.core.util.crypto.AttachmentSecret;
|
||||||
import org.thoughtcrime.securesms.crypto.ModernEncryptingPartOutputStream;
|
import org.signal.core.util.crypto.ModernEncryptingPartOutputStream;
|
||||||
import org.thoughtcrime.securesms.database.AttachmentTable;
|
import org.thoughtcrime.securesms.database.AttachmentTable;
|
||||||
import org.thoughtcrime.securesms.database.EmojiSearchTable;
|
import org.thoughtcrime.securesms.database.EmojiSearchTable;
|
||||||
import org.thoughtcrime.securesms.database.KeyValueDatabase;
|
import org.thoughtcrime.securesms.database.KeyValueDatabase;
|
||||||
|
|||||||
@ -16,13 +16,13 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.flowOn
|
import kotlinx.coroutines.flow.flowOn
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
|
import org.signal.core.models.database.AttachmentId
|
||||||
import org.signal.core.util.bytes
|
import org.signal.core.util.bytes
|
||||||
import org.signal.core.util.concurrent.SignalExecutors
|
import org.signal.core.util.concurrent.SignalExecutors
|
||||||
import org.signal.core.util.logging.Log
|
import org.signal.core.util.logging.Log
|
||||||
import org.signal.core.util.safeUnregisterReceiver
|
import org.signal.core.util.safeUnregisterReceiver
|
||||||
import org.signal.core.util.throttleLatest
|
import org.signal.core.util.throttleLatest
|
||||||
import org.thoughtcrime.securesms.BuildConfig
|
import org.thoughtcrime.securesms.BuildConfig
|
||||||
import org.thoughtcrime.securesms.attachments.AttachmentId
|
|
||||||
import org.thoughtcrime.securesms.backup.RestoreState
|
import org.thoughtcrime.securesms.backup.RestoreState
|
||||||
import org.thoughtcrime.securesms.database.DatabaseObserver
|
import org.thoughtcrime.securesms.database.DatabaseObserver
|
||||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||||
|
|||||||
@ -34,6 +34,7 @@ import org.signal.core.models.backup.BackupId
|
|||||||
import org.signal.core.models.backup.MediaName
|
import org.signal.core.models.backup.MediaName
|
||||||
import org.signal.core.models.backup.MediaRootBackupKey
|
import org.signal.core.models.backup.MediaRootBackupKey
|
||||||
import org.signal.core.models.backup.MessageBackupKey
|
import org.signal.core.models.backup.MessageBackupKey
|
||||||
|
import org.signal.core.models.database.AttachmentId
|
||||||
import org.signal.core.util.Base64
|
import org.signal.core.util.Base64
|
||||||
import org.signal.core.util.Base64.decodeBase64OrThrow
|
import org.signal.core.util.Base64.decodeBase64OrThrow
|
||||||
import org.signal.core.util.CursorUtil
|
import org.signal.core.util.CursorUtil
|
||||||
@ -46,6 +47,7 @@ import org.signal.core.util.bytes
|
|||||||
import org.signal.core.util.concurrent.LimitedWorker
|
import org.signal.core.util.concurrent.LimitedWorker
|
||||||
import org.signal.core.util.concurrent.SignalDispatchers
|
import org.signal.core.util.concurrent.SignalDispatchers
|
||||||
import org.signal.core.util.concurrent.SignalExecutors
|
import org.signal.core.util.concurrent.SignalExecutors
|
||||||
|
import org.signal.core.util.crypto.AttachmentSecretProvider
|
||||||
import org.signal.core.util.decodeOrNull
|
import org.signal.core.util.decodeOrNull
|
||||||
import org.signal.core.util.forceForeignKeyConstraintsEnabled
|
import org.signal.core.util.forceForeignKeyConstraintsEnabled
|
||||||
import org.signal.core.util.fullWalCheckpoint
|
import org.signal.core.util.fullWalCheckpoint
|
||||||
@ -72,9 +74,7 @@ import org.signal.network.NetworkResult
|
|||||||
import org.signal.network.StatusCodeErrorAction
|
import org.signal.network.StatusCodeErrorAction
|
||||||
import org.signal.network.api.SvrBApi
|
import org.signal.network.api.SvrBApi
|
||||||
import org.signal.network.exceptions.NonSuccessfulResponseCodeException
|
import org.signal.network.exceptions.NonSuccessfulResponseCodeException
|
||||||
import org.signal.network.rest.toNetworkResult
|
|
||||||
import org.thoughtcrime.securesms.R
|
import org.thoughtcrime.securesms.R
|
||||||
import org.thoughtcrime.securesms.attachments.AttachmentId
|
|
||||||
import org.thoughtcrime.securesms.attachments.Cdn
|
import org.thoughtcrime.securesms.attachments.Cdn
|
||||||
import org.thoughtcrime.securesms.attachments.DatabaseAttachment
|
import org.thoughtcrime.securesms.attachments.DatabaseAttachment
|
||||||
import org.thoughtcrime.securesms.backup.ArchiveUploadProgress
|
import org.thoughtcrime.securesms.backup.ArchiveUploadProgress
|
||||||
@ -94,7 +94,7 @@ import org.thoughtcrime.securesms.backup.v2.ui.BackupAlert
|
|||||||
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType
|
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType
|
||||||
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity
|
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity
|
||||||
import org.thoughtcrime.securesms.components.settings.app.subscription.RecurringInAppPaymentRepository
|
import org.thoughtcrime.securesms.components.settings.app.subscription.RecurringInAppPaymentRepository
|
||||||
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider
|
import org.thoughtcrime.securesms.crypto.AppAttachmentSecretStore
|
||||||
import org.thoughtcrime.securesms.crypto.DatabaseSecretProvider
|
import org.thoughtcrime.securesms.crypto.DatabaseSecretProvider
|
||||||
import org.thoughtcrime.securesms.database.AttachmentTable
|
import org.thoughtcrime.securesms.database.AttachmentTable
|
||||||
import org.thoughtcrime.securesms.database.BackupMediaSnapshotTable.ArchiveMediaItem
|
import org.thoughtcrime.securesms.database.BackupMediaSnapshotTable.ArchiveMediaItem
|
||||||
@ -141,7 +141,6 @@ import org.thoughtcrime.securesms.logsubmit.SubmitDebugLogRepository
|
|||||||
import org.thoughtcrime.securesms.net.SignalNetwork
|
import org.thoughtcrime.securesms.net.SignalNetwork
|
||||||
import org.thoughtcrime.securesms.notifications.NotificationChannels
|
import org.thoughtcrime.securesms.notifications.NotificationChannels
|
||||||
import org.thoughtcrime.securesms.notifications.NotificationIds
|
import org.thoughtcrime.securesms.notifications.NotificationIds
|
||||||
import org.thoughtcrime.securesms.providers.BlobProvider
|
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||||
import org.thoughtcrime.securesms.service.BackupMediaRestoreService
|
import org.thoughtcrime.securesms.service.BackupMediaRestoreService
|
||||||
@ -718,7 +717,7 @@ object BackupRepository {
|
|||||||
SignalDatabase(
|
SignalDatabase(
|
||||||
context = context,
|
context = context,
|
||||||
databaseSecret = DatabaseSecretProvider.getOrCreateDatabaseSecret(context),
|
databaseSecret = DatabaseSecretProvider.getOrCreateDatabaseSecret(context),
|
||||||
attachmentSecret = AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret(),
|
attachmentSecret = AttachmentSecretProvider.getInstance(context, AppAttachmentSecretStore).getOrCreateAttachmentSecret(),
|
||||||
name = "$baseName.db"
|
name = "$baseName.db"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -2236,7 +2235,7 @@ object BackupRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Log.i(TAG, "[remoteRestore] Downloading backup")
|
Log.i(TAG, "[remoteRestore] Downloading backup")
|
||||||
val tempBackupFile = BlobProvider.getInstance().forNonAutoEncryptingSingleSessionOnDisk(AppDependencies.application)
|
val tempBackupFile = AppDependencies.blobs.forNonAutoEncryptingSingleSessionOnDisk(AppDependencies.application)
|
||||||
when (val result = downloadBackupFile(tempBackupFile, progressListener)) {
|
when (val result = downloadBackupFile(tempBackupFile, progressListener)) {
|
||||||
is NetworkResult.Success -> Log.i(TAG, "[remoteRestore] Download successful")
|
is NetworkResult.Success -> Log.i(TAG, "[remoteRestore] Download successful")
|
||||||
else -> {
|
else -> {
|
||||||
@ -2356,7 +2355,7 @@ object BackupRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Log.i(TAG, "[restoreLinkAndSyncBackup] Downloading backup")
|
Log.i(TAG, "[restoreLinkAndSyncBackup] Downloading backup")
|
||||||
val tempBackupFile = BlobProvider.getInstance().forNonAutoEncryptingSingleSessionOnDisk(AppDependencies.application)
|
val tempBackupFile = AppDependencies.blobs.forNonAutoEncryptingSingleSessionOnDisk(AppDependencies.application)
|
||||||
when (val result = AppDependencies.signalServiceMessageReceiver.retrieveLinkAndSyncBackup(response.cdn, response.key, tempBackupFile, progressListener)) {
|
when (val result = AppDependencies.signalServiceMessageReceiver.retrieveLinkAndSyncBackup(response.cdn, response.key, tempBackupFile, progressListener)) {
|
||||||
is NetworkResult.Success -> Log.i(TAG, "[restoreLinkAndSyncBackup] Download successful")
|
is NetworkResult.Success -> Log.i(TAG, "[restoreLinkAndSyncBackup] Download successful")
|
||||||
else -> {
|
else -> {
|
||||||
@ -2559,6 +2558,9 @@ enum class BackupMode {
|
|||||||
|
|
||||||
val isLocalBackup: Boolean
|
val isLocalBackup: Boolean
|
||||||
get() = this == LOCAL
|
get() = this == LOCAL
|
||||||
|
|
||||||
|
val isPlaintextExport: Boolean
|
||||||
|
get() = this == PLAINTEXT_EXPORT
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -5,8 +5,8 @@
|
|||||||
|
|
||||||
package org.thoughtcrime.securesms.backup.v2
|
package org.thoughtcrime.securesms.backup.v2
|
||||||
|
|
||||||
|
import org.signal.core.models.database.AttachmentId
|
||||||
import org.signal.core.util.concurrent.SignalExecutors
|
import org.signal.core.util.concurrent.SignalExecutors
|
||||||
import org.thoughtcrime.securesms.attachments.AttachmentId
|
|
||||||
import org.thoughtcrime.securesms.attachments.DatabaseAttachment
|
import org.thoughtcrime.securesms.attachments.DatabaseAttachment
|
||||||
import org.thoughtcrime.securesms.database.AttachmentTable
|
import org.thoughtcrime.securesms.database.AttachmentTable
|
||||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
package org.thoughtcrime.securesms.backup.v2
|
package org.thoughtcrime.securesms.backup.v2
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.attachments.AttachmentId
|
import org.signal.core.models.database.AttachmentId
|
||||||
import org.whispersystems.signalservice.api.archive.BatchArchiveMediaResponse
|
import org.whispersystems.signalservice.api.archive.BatchArchiveMediaResponse
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -5,8 +5,8 @@
|
|||||||
|
|
||||||
package org.thoughtcrime.securesms.backup.v2.database
|
package org.thoughtcrime.securesms.backup.v2.database
|
||||||
|
|
||||||
|
import org.signal.core.models.database.AttachmentId
|
||||||
import org.thoughtcrime.securesms.attachments.Attachment
|
import org.thoughtcrime.securesms.attachments.Attachment
|
||||||
import org.thoughtcrime.securesms.attachments.AttachmentId
|
|
||||||
import org.thoughtcrime.securesms.database.AttachmentTable
|
import org.thoughtcrime.securesms.database.AttachmentTable
|
||||||
|
|
||||||
fun AttachmentTable.restoreWallpaperAttachment(attachment: Attachment): AttachmentId? {
|
fun AttachmentTable.restoreWallpaperAttachment(attachment: Attachment): AttachmentId? {
|
||||||
|
|||||||
@ -167,7 +167,11 @@ fun MessageTable.getMessagesForBackup(db: SignalDatabase, backupTime: Long, self
|
|||||||
.where(
|
.where(
|
||||||
buildString {
|
buildString {
|
||||||
append("$STORY_TYPE = 0 AND $PARENT_STORY_ID <= 0 AND $SCHEDULED_DATE = -1 AND ")
|
append("$STORY_TYPE = 0 AND $PARENT_STORY_ID <= 0 AND $SCHEDULED_DATE = -1 AND ")
|
||||||
append("($EXPIRES_IN == 0 OR $EXPIRES_IN > ${1.days.inWholeMilliseconds})")
|
if (exportState.backupMode.isPlaintextExport) {
|
||||||
|
append("$EXPIRES_IN == 0")
|
||||||
|
} else {
|
||||||
|
append("($EXPIRES_IN == 0 OR $EXPIRES_IN > ${1.days.inWholeMilliseconds})")
|
||||||
|
}
|
||||||
append(" AND $DATE_RECEIVED >= $lastSeenReceivedTime $cutoffQuery")
|
append(" AND $DATE_RECEIVED >= $lastSeenReceivedTime $cutoffQuery")
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@ -42,6 +42,7 @@ import org.signal.archive.proto.Text
|
|||||||
import org.signal.archive.proto.ThreadMergeChatUpdate
|
import org.signal.archive.proto.ThreadMergeChatUpdate
|
||||||
import org.signal.archive.proto.ViewOnceMessage
|
import org.signal.archive.proto.ViewOnceMessage
|
||||||
import org.signal.core.models.ServiceId
|
import org.signal.core.models.ServiceId
|
||||||
|
import org.signal.core.models.database.AttachmentId
|
||||||
import org.signal.core.util.Base64
|
import org.signal.core.util.Base64
|
||||||
import org.signal.core.util.EventTimer
|
import org.signal.core.util.EventTimer
|
||||||
import org.signal.core.util.Hex
|
import org.signal.core.util.Hex
|
||||||
@ -68,7 +69,6 @@ import org.signal.core.util.requireLong
|
|||||||
import org.signal.core.util.requireLongOrNull
|
import org.signal.core.util.requireLongOrNull
|
||||||
import org.signal.core.util.requireString
|
import org.signal.core.util.requireString
|
||||||
import org.signal.core.util.toByteArray
|
import org.signal.core.util.toByteArray
|
||||||
import org.thoughtcrime.securesms.attachments.AttachmentId
|
|
||||||
import org.thoughtcrime.securesms.attachments.DatabaseAttachment
|
import org.thoughtcrime.securesms.attachments.DatabaseAttachment
|
||||||
import org.thoughtcrime.securesms.backup.v2.BackupMode
|
import org.thoughtcrime.securesms.backup.v2.BackupMode
|
||||||
import org.thoughtcrime.securesms.backup.v2.ExportOddities
|
import org.thoughtcrime.securesms.backup.v2.ExportOddities
|
||||||
@ -647,15 +647,22 @@ private fun BackupMessageRecord.toBasicChatItemBuilder(selfRecipientId: Recipien
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!MessageTypes.isExpirationTimerUpdate(record.type) && builder.expiresInMs != null && builder.expireStartDate != null) {
|
if (!MessageTypes.isExpirationTimerUpdate(record.type) && builder.expiresInMs != null) {
|
||||||
val cutoffDuration = ChatItemArchiveExporter.EXPIRATION_CUTOFF.inWholeMilliseconds
|
if (exportState.backupMode.isPlaintextExport) {
|
||||||
val expiresAt = builder.expireStartDate!! + builder.expiresInMs!!
|
|
||||||
val threshold = if (exportState.backupMode.isLinkAndSync) backupStartTime else backupStartTime + cutoffDuration
|
|
||||||
|
|
||||||
if (expiresAt < threshold || (builder.expiresInMs!! <= cutoffDuration && !exportState.backupMode.isLinkAndSync)) {
|
|
||||||
Log.w(TAG, ExportSkips.messageExpiresTooSoon(record.dateSent))
|
Log.w(TAG, ExportSkips.messageExpiresTooSoon(record.dateSent))
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (builder.expireStartDate != null) {
|
||||||
|
val cutoffDuration = ChatItemArchiveExporter.EXPIRATION_CUTOFF.inWholeMilliseconds
|
||||||
|
val expiresAt = builder.expireStartDate!! + builder.expiresInMs!!
|
||||||
|
val threshold = if (exportState.backupMode.isLinkAndSync) backupStartTime else backupStartTime + cutoffDuration
|
||||||
|
|
||||||
|
if (expiresAt < threshold || (builder.expiresInMs!! <= cutoffDuration && !exportState.backupMode.isLinkAndSync)) {
|
||||||
|
Log.w(TAG, ExportSkips.messageExpiresTooSoon(record.dateSent))
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (builder.expireStartDate != null && builder.expiresInMs == null) {
|
if (builder.expireStartDate != null && builder.expiresInMs == null) {
|
||||||
|
|||||||
@ -7,10 +7,10 @@ package org.thoughtcrime.securesms.backup.v2.importer
|
|||||||
|
|
||||||
import androidx.core.content.contentValuesOf
|
import androidx.core.content.contentValuesOf
|
||||||
import org.signal.archive.proto.Chat
|
import org.signal.archive.proto.Chat
|
||||||
|
import org.signal.core.models.database.AttachmentId
|
||||||
import org.signal.core.util.SqlUtil
|
import org.signal.core.util.SqlUtil
|
||||||
import org.signal.core.util.insertInto
|
import org.signal.core.util.insertInto
|
||||||
import org.signal.core.util.toInt
|
import org.signal.core.util.toInt
|
||||||
import org.thoughtcrime.securesms.attachments.AttachmentId
|
|
||||||
import org.thoughtcrime.securesms.backup.v2.ImportState
|
import org.thoughtcrime.securesms.backup.v2.ImportState
|
||||||
import org.thoughtcrime.securesms.backup.v2.database.restoreWallpaperAttachment
|
import org.thoughtcrime.securesms.backup.v2.database.restoreWallpaperAttachment
|
||||||
import org.thoughtcrime.securesms.backup.v2.util.parseChatWallpaper
|
import org.thoughtcrime.securesms.backup.v2.util.parseChatWallpaper
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import org.signal.archive.stream.EncryptedBackupReader
|
|||||||
import org.signal.core.models.backup.BackupId
|
import org.signal.core.models.backup.BackupId
|
||||||
import org.signal.core.models.backup.MediaName
|
import org.signal.core.models.backup.MediaName
|
||||||
import org.signal.core.models.backup.MessageBackupKey
|
import org.signal.core.models.backup.MessageBackupKey
|
||||||
|
import org.signal.core.models.database.AttachmentId
|
||||||
import org.signal.core.util.Stopwatch
|
import org.signal.core.util.Stopwatch
|
||||||
import org.signal.core.util.StreamUtil
|
import org.signal.core.util.StreamUtil
|
||||||
import org.signal.core.util.Util
|
import org.signal.core.util.Util
|
||||||
@ -23,7 +24,6 @@ import org.signal.core.util.logging.Log
|
|||||||
import org.signal.core.util.readFully
|
import org.signal.core.util.readFully
|
||||||
import org.signal.core.util.toJson
|
import org.signal.core.util.toJson
|
||||||
import org.signal.libsignal.crypto.Aes256Ctr32
|
import org.signal.libsignal.crypto.Aes256Ctr32
|
||||||
import org.thoughtcrime.securesms.attachments.AttachmentId
|
|
||||||
import org.thoughtcrime.securesms.backup.LocalExportProgress
|
import org.thoughtcrime.securesms.backup.LocalExportProgress
|
||||||
import org.thoughtcrime.securesms.backup.v2.BackupRepository
|
import org.thoughtcrime.securesms.backup.v2.BackupRepository
|
||||||
import org.thoughtcrime.securesms.database.AttachmentTable
|
import org.thoughtcrime.securesms.database.AttachmentTable
|
||||||
|
|||||||
@ -12,11 +12,12 @@ import org.signal.archive.proto.AccountData
|
|||||||
import org.signal.archive.proto.ChatStyle
|
import org.signal.archive.proto.ChatStyle
|
||||||
import org.signal.archive.proto.Frame
|
import org.signal.archive.proto.Frame
|
||||||
import org.signal.archive.stream.BackupFrameEmitter
|
import org.signal.archive.stream.BackupFrameEmitter
|
||||||
|
import org.signal.core.models.database.AttachmentId
|
||||||
import org.signal.core.util.UuidUtil
|
import org.signal.core.util.UuidUtil
|
||||||
import org.signal.core.util.logging.Log
|
import org.signal.core.util.logging.Log
|
||||||
import org.signal.core.util.toByteArray
|
import org.signal.core.util.toByteArray
|
||||||
import org.signal.libsignal.zkgroup.backups.BackupLevel
|
import org.signal.libsignal.zkgroup.backups.BackupLevel
|
||||||
import org.thoughtcrime.securesms.attachments.AttachmentId
|
import org.signal.mediasend.SentMediaQuality
|
||||||
import org.thoughtcrime.securesms.backup.v2.ExportState
|
import org.thoughtcrime.securesms.backup.v2.ExportState
|
||||||
import org.thoughtcrime.securesms.backup.v2.ImportState
|
import org.thoughtcrime.securesms.backup.v2.ImportState
|
||||||
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
|
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
|
||||||
@ -471,19 +472,19 @@ object AccountDataArchiveProcessor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun org.thoughtcrime.securesms.mms.SentMediaQuality.toRemoteSentMediaQuality(): AccountData.SentMediaQuality {
|
private fun SentMediaQuality.toRemoteSentMediaQuality(): AccountData.SentMediaQuality {
|
||||||
return when (this) {
|
return when (this) {
|
||||||
org.thoughtcrime.securesms.mms.SentMediaQuality.STANDARD -> AccountData.SentMediaQuality.STANDARD
|
SentMediaQuality.STANDARD -> AccountData.SentMediaQuality.STANDARD
|
||||||
org.thoughtcrime.securesms.mms.SentMediaQuality.HIGH -> AccountData.SentMediaQuality.HIGH
|
SentMediaQuality.HIGH -> AccountData.SentMediaQuality.HIGH
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun AccountData.SentMediaQuality?.toLocalSentMediaQuality(): org.thoughtcrime.securesms.mms.SentMediaQuality {
|
private fun AccountData.SentMediaQuality?.toLocalSentMediaQuality(): SentMediaQuality {
|
||||||
return when (this) {
|
return when (this) {
|
||||||
AccountData.SentMediaQuality.HIGH -> org.thoughtcrime.securesms.mms.SentMediaQuality.HIGH
|
AccountData.SentMediaQuality.HIGH -> SentMediaQuality.HIGH
|
||||||
AccountData.SentMediaQuality.STANDARD -> org.thoughtcrime.securesms.mms.SentMediaQuality.STANDARD
|
AccountData.SentMediaQuality.STANDARD -> SentMediaQuality.STANDARD
|
||||||
AccountData.SentMediaQuality.UNKNOWN_QUALITY -> org.thoughtcrime.securesms.mms.SentMediaQuality.STANDARD
|
AccountData.SentMediaQuality.UNKNOWN_QUALITY -> SentMediaQuality.STANDARD
|
||||||
null -> org.thoughtcrime.securesms.mms.SentMediaQuality.STANDARD
|
null -> SentMediaQuality.STANDARD
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,85 @@
|
|||||||
|
package org.thoughtcrime.securesms.backup.v2.ui.subscription
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
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.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.res.vectorResource
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import org.signal.core.ui.compose.Buttons
|
||||||
|
import org.signal.core.ui.compose.DayNightPreviews
|
||||||
|
import org.signal.core.ui.compose.Previews
|
||||||
|
import org.signal.core.ui.compose.horizontalGutters
|
||||||
|
import org.thoughtcrime.securesms.R
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bottom sheet shown when confirming your recovery key after saving to password manager
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun ConfirmRecoveryKeySheet(
|
||||||
|
onConfirm: () -> Unit = {},
|
||||||
|
onSeeAgain: () -> Unit = {},
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = modifier.horizontalGutters(),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = ImageVector.vectorResource(R.drawable.backup_confirm_80),
|
||||||
|
tint = Color.Unspecified,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.padding(top = 24.dp, bottom = 16.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.MessageBackupsKeyVerifyScreen__confirm_your_backup_key),
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.padding(bottom = 12.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.MessageBackupsKeyRecordScreen__confirm_that_your_recovery),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.size(60.dp))
|
||||||
|
|
||||||
|
Buttons.LargeTonal(onClick = onConfirm) {
|
||||||
|
Text(text = stringResource(R.string.MessageBackupsKeyRecordScreen__confirm_recovery))
|
||||||
|
}
|
||||||
|
|
||||||
|
TextButton(
|
||||||
|
onClick = onSeeAgain,
|
||||||
|
modifier = Modifier.padding(vertical = 16.dp)
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(R.string.MessageBackupsKeyRecordScreen__see_key_again))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@DayNightPreviews
|
||||||
|
@Composable
|
||||||
|
private fun ConfirmRecoveryKeyPreview() {
|
||||||
|
Previews.BottomSheetContentPreview {
|
||||||
|
ConfirmRecoveryKeySheet(
|
||||||
|
onConfirm = {},
|
||||||
|
onSeeAgain = {},
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -68,7 +68,8 @@ class MessageBackupsFlowFragment : ComposeFragment(), InAppPaymentCheckoutDelega
|
|||||||
private val viewModel: MessageBackupsFlowViewModel by viewModel {
|
private val viewModel: MessageBackupsFlowViewModel by viewModel {
|
||||||
MessageBackupsFlowViewModel(
|
MessageBackupsFlowViewModel(
|
||||||
initialTierSelection = requireArguments().getSerializableCompat(TIER, MessageBackupTier::class.java),
|
initialTierSelection = requireArguments().getSerializableCompat(TIER, MessageBackupTier::class.java),
|
||||||
googlePlayApiAvailability = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(requireContext())
|
googlePlayApiAvailability = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(requireContext()),
|
||||||
|
isCredentialManagerSupported = AndroidCredentialRepository.isCredentialManagerSupported(requireContext())
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -155,6 +156,32 @@ class MessageBackupsFlowFragment : ComposeFragment(), InAppPaymentCheckoutDelega
|
|||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val passwordManagerSettingsIntent = AndroidCredentialRepository.getCredentialManagerSettingsIntent(requireContext())
|
val passwordManagerSettingsIntent = AndroidCredentialRepository.getCredentialManagerSettingsIntent(requireContext())
|
||||||
|
|
||||||
|
MessageBackupsKeyRecordScreen(
|
||||||
|
backupKey = state.accountEntropyPool.displayValue,
|
||||||
|
keySaveState = state.backupKeySaveState,
|
||||||
|
canOpenPasswordManagerSettings = passwordManagerSettingsIntent != null,
|
||||||
|
onNavigationClick = viewModel::goToPreviousStage,
|
||||||
|
mode = remember {
|
||||||
|
MessageBackupsKeyRecordMode.Passkey(
|
||||||
|
onSaveToPasswordManager = viewModel::onBackupKeySaveRequested,
|
||||||
|
onSaveManually = viewModel::goToRecordManually,
|
||||||
|
onSaveSuccessful = viewModel::goToNextStage
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onCopyToClipboardClick = { Util.copyToClipboard(context, it, CLIPBOARD_TIMEOUT_SECONDS) },
|
||||||
|
onRequestSaveToPasswordManager = viewModel::onBackupKeySaveRequested,
|
||||||
|
onConfirmSaveToPasswordManager = viewModel::onBackupKeySaveConfirmed,
|
||||||
|
onSaveStateCleared = viewModel::onBackupKeySaveStateCleared,
|
||||||
|
onSaveToPasswordManagerComplete = viewModel::onBackupKeySaveCompleted,
|
||||||
|
onGoToPasswordManagerSettingsClick = { requireContext().startActivity(passwordManagerSettingsIntent) },
|
||||||
|
notifyKeyIsSameAsOnDeviceBackupKey = SignalStore.backup.newLocalBackupsEnabled
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable(route = MessageBackupsStage.Route.BACKUP_KEY_RECORD_MANUALLY.name) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val passwordManagerSettingsIntent = AndroidCredentialRepository.getCredentialManagerSettingsIntent(requireContext())
|
||||||
|
|
||||||
MessageBackupsKeyRecordScreen(
|
MessageBackupsKeyRecordScreen(
|
||||||
backupKey = state.accountEntropyPool.displayValue,
|
backupKey = state.accountEntropyPool.displayValue,
|
||||||
keySaveState = state.backupKeySaveState,
|
keySaveState = state.backupKeySaveState,
|
||||||
|
|||||||
@ -53,6 +53,7 @@ import kotlin.time.Duration.Companion.seconds
|
|||||||
class MessageBackupsFlowViewModel(
|
class MessageBackupsFlowViewModel(
|
||||||
private val initialTierSelection: MessageBackupTier?,
|
private val initialTierSelection: MessageBackupTier?,
|
||||||
googlePlayApiAvailability: Int,
|
googlePlayApiAvailability: Int,
|
||||||
|
private val isCredentialManagerSupported: Boolean,
|
||||||
startScreen: MessageBackupsStage = if (SignalStore.backup.backupTier == null) MessageBackupsStage.EDUCATION else MessageBackupsStage.TYPE_SELECTION
|
startScreen: MessageBackupsStage = if (SignalStore.backup.backupTier == null) MessageBackupsStage.EDUCATION else MessageBackupsStage.TYPE_SELECTION
|
||||||
) : ViewModel(), BackupKeyCredentialManagerHandler {
|
) : ViewModel(), BackupKeyCredentialManagerHandler {
|
||||||
|
|
||||||
@ -238,8 +239,9 @@ class MessageBackupsFlowViewModel(
|
|||||||
when (it.stage) {
|
when (it.stage) {
|
||||||
MessageBackupsStage.CANCEL -> error("Unsupported state transition from terminal state CANCEL")
|
MessageBackupsStage.CANCEL -> error("Unsupported state transition from terminal state CANCEL")
|
||||||
MessageBackupsStage.EDUCATION -> it.copy(stage = MessageBackupsStage.BACKUP_KEY_EDUCATION)
|
MessageBackupsStage.EDUCATION -> it.copy(stage = MessageBackupsStage.BACKUP_KEY_EDUCATION)
|
||||||
MessageBackupsStage.BACKUP_KEY_EDUCATION -> it.copy(stage = MessageBackupsStage.BACKUP_KEY_RECORD)
|
MessageBackupsStage.BACKUP_KEY_EDUCATION -> it.copy(stage = if (isCredentialManagerSupported) MessageBackupsStage.BACKUP_KEY_RECORD else MessageBackupsStage.BACKUP_KEY_RECORD_MANUALLY)
|
||||||
MessageBackupsStage.BACKUP_KEY_RECORD -> it.copy(stage = MessageBackupsStage.BACKUP_KEY_VERIFY)
|
MessageBackupsStage.BACKUP_KEY_RECORD -> it.copy(stage = MessageBackupsStage.TYPE_SELECTION)
|
||||||
|
MessageBackupsStage.BACKUP_KEY_RECORD_MANUALLY -> it.copy(stage = MessageBackupsStage.BACKUP_KEY_VERIFY)
|
||||||
MessageBackupsStage.BACKUP_KEY_VERIFY -> it.copy(stage = MessageBackupsStage.TYPE_SELECTION)
|
MessageBackupsStage.BACKUP_KEY_VERIFY -> it.copy(stage = MessageBackupsStage.TYPE_SELECTION)
|
||||||
MessageBackupsStage.TYPE_SELECTION -> validateTypeAndUpdateState(it)
|
MessageBackupsStage.TYPE_SELECTION -> validateTypeAndUpdateState(it)
|
||||||
MessageBackupsStage.CHECKOUT_SHEET -> it.copy(stage = MessageBackupsStage.PROCESS_PAYMENT)
|
MessageBackupsStage.CHECKOUT_SHEET -> it.copy(stage = MessageBackupsStage.PROCESS_PAYMENT)
|
||||||
@ -262,7 +264,8 @@ class MessageBackupsFlowViewModel(
|
|||||||
MessageBackupsStage.EDUCATION -> MessageBackupsStage.CANCEL
|
MessageBackupsStage.EDUCATION -> MessageBackupsStage.CANCEL
|
||||||
MessageBackupsStage.BACKUP_KEY_EDUCATION -> MessageBackupsStage.EDUCATION
|
MessageBackupsStage.BACKUP_KEY_EDUCATION -> MessageBackupsStage.EDUCATION
|
||||||
MessageBackupsStage.BACKUP_KEY_RECORD -> MessageBackupsStage.BACKUP_KEY_EDUCATION
|
MessageBackupsStage.BACKUP_KEY_RECORD -> MessageBackupsStage.BACKUP_KEY_EDUCATION
|
||||||
MessageBackupsStage.BACKUP_KEY_VERIFY -> MessageBackupsStage.BACKUP_KEY_RECORD
|
MessageBackupsStage.BACKUP_KEY_RECORD_MANUALLY -> if (isCredentialManagerSupported) MessageBackupsStage.BACKUP_KEY_RECORD else MessageBackupsStage.BACKUP_KEY_EDUCATION
|
||||||
|
MessageBackupsStage.BACKUP_KEY_VERIFY -> MessageBackupsStage.BACKUP_KEY_RECORD_MANUALLY
|
||||||
MessageBackupsStage.TYPE_SELECTION -> MessageBackupsStage.BACKUP_KEY_RECORD
|
MessageBackupsStage.TYPE_SELECTION -> MessageBackupsStage.BACKUP_KEY_RECORD
|
||||||
MessageBackupsStage.CHECKOUT_SHEET -> MessageBackupsStage.TYPE_SELECTION
|
MessageBackupsStage.CHECKOUT_SHEET -> MessageBackupsStage.TYPE_SELECTION
|
||||||
MessageBackupsStage.CREATING_IN_APP_PAYMENT -> MessageBackupsStage.CREATING_IN_APP_PAYMENT
|
MessageBackupsStage.CREATING_IN_APP_PAYMENT -> MessageBackupsStage.CREATING_IN_APP_PAYMENT
|
||||||
@ -277,6 +280,12 @@ class MessageBackupsFlowViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun goToRecordManually() {
|
||||||
|
internalStateFlow.update {
|
||||||
|
it.copy(stage = MessageBackupsStage.BACKUP_KEY_RECORD_MANUALLY)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun onMessageBackupTierUpdated(messageBackupTier: MessageBackupTier) {
|
fun onMessageBackupTierUpdated(messageBackupTier: MessageBackupTier) {
|
||||||
internalStateFlow.update {
|
internalStateFlow.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
|
|||||||
@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
package org.thoughtcrime.securesms.backup.v2.ui.subscription
|
package org.thoughtcrime.securesms.backup.v2.ui.subscription
|
||||||
|
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.annotation.DrawableRes
|
import androidx.annotation.DrawableRes
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
@ -28,6 +29,7 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.dimensionResource
|
import androidx.compose.ui.res.dimensionResource
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
@ -46,6 +48,7 @@ import org.signal.core.ui.compose.SignalIcons
|
|||||||
import org.signal.core.ui.compose.horizontalGutters
|
import org.signal.core.ui.compose.horizontalGutters
|
||||||
import org.signal.core.ui.compose.theme.SignalTheme
|
import org.signal.core.ui.compose.theme.SignalTheme
|
||||||
import org.thoughtcrime.securesms.R
|
import org.thoughtcrime.securesms.R
|
||||||
|
import org.thoughtcrime.securesms.components.compose.rememberBiometricsAuthentication
|
||||||
import org.signal.core.ui.R as CoreUiR
|
import org.signal.core.ui.R as CoreUiR
|
||||||
|
|
||||||
enum class MessageBackupsKeyEducationScreenMode {
|
enum class MessageBackupsKeyEducationScreenMode {
|
||||||
@ -80,6 +83,14 @@ fun MessageBackupsKeyEducationScreen(
|
|||||||
mode: MessageBackupsKeyEducationScreenMode = MessageBackupsKeyEducationScreenMode.DEFAULT
|
mode: MessageBackupsKeyEducationScreenMode = MessageBackupsKeyEducationScreenMode.DEFAULT
|
||||||
) {
|
) {
|
||||||
val scrollState = rememberScrollState()
|
val scrollState = rememberScrollState()
|
||||||
|
val context = LocalContext.current
|
||||||
|
val biometrics = rememberBiometricsAuthentication(
|
||||||
|
promptTitle = stringResource(R.string.RemoteBackupsSettingsFragment__unlock_to_view_backup_key),
|
||||||
|
educationSheetMessage = stringResource(R.string.RemoteBackupsSettingsFragment__to_view_your_key),
|
||||||
|
onAuthenticationFailed = {
|
||||||
|
Toast.makeText(context, R.string.RemoteBackupsSettingsFragment__authentication_required, Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
Scaffolds.Settings(
|
Scaffolds.Settings(
|
||||||
title = "",
|
title = "",
|
||||||
@ -139,7 +150,7 @@ fun MessageBackupsKeyEducationScreen(
|
|||||||
.padding(top = 16.dp, bottom = 24.dp)
|
.padding(top = 16.dp, bottom = 24.dp)
|
||||||
) {
|
) {
|
||||||
Buttons.LargeTonal(
|
Buttons.LargeTonal(
|
||||||
onClick = onNextClick,
|
onClick = { biometrics.withBiometricsAuthentication(onNextClick) },
|
||||||
modifier = Modifier.align(Alignment.Center)
|
modifier = Modifier.align(Alignment.Center)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
|
|||||||
@ -6,14 +6,22 @@
|
|||||||
package org.thoughtcrime.securesms.backup.v2.ui.subscription
|
package org.thoughtcrime.securesms.backup.v2.ui.subscription
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
|
import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
|
||||||
import androidx.annotation.UiContext
|
import androidx.annotation.UiContext
|
||||||
|
import androidx.compose.animation.AnimatedContent
|
||||||
|
import androidx.compose.animation.SizeTransform
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.animation.togetherWith
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.ColumnScope
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
@ -22,6 +30,7 @@ import androidx.compose.foundation.layout.size
|
|||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.ModalBottomSheet
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
@ -34,9 +43,16 @@ import androidx.compose.runtime.Stable
|
|||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.drawWithContent
|
||||||
|
import androidx.compose.ui.graphics.BlendMode
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.CompositingStrategy
|
||||||
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.testTag
|
import androidx.compose.ui.platform.testTag
|
||||||
@ -48,6 +64,7 @@ import androidx.compose.ui.text.style.TextAlign
|
|||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import org.signal.core.ui.compose.BottomSheets
|
import org.signal.core.ui.compose.BottomSheets
|
||||||
import org.signal.core.ui.compose.Buttons
|
import org.signal.core.ui.compose.Buttons
|
||||||
import org.signal.core.ui.compose.DayNightPreviews
|
import org.signal.core.ui.compose.DayNightPreviews
|
||||||
@ -85,6 +102,11 @@ sealed interface MessageBackupsKeyRecordMode {
|
|||||||
val isOptimizedStorageEnabled: Boolean,
|
val isOptimizedStorageEnabled: Boolean,
|
||||||
val canRotateKey: Boolean
|
val canRotateKey: Boolean
|
||||||
) : MessageBackupsKeyRecordMode
|
) : MessageBackupsKeyRecordMode
|
||||||
|
data class Passkey(
|
||||||
|
val onSaveToPasswordManager: () -> Unit,
|
||||||
|
val onSaveManually: () -> Unit,
|
||||||
|
val onSaveSuccessful: () -> Unit
|
||||||
|
) : MessageBackupsKeyRecordMode
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -136,18 +158,24 @@ fun MessageBackupsKeyRecordScreen(
|
|||||||
onRequestSaveToPasswordManager: () -> Unit = {},
|
onRequestSaveToPasswordManager: () -> Unit = {},
|
||||||
onConfirmSaveToPasswordManager: () -> Unit = {},
|
onConfirmSaveToPasswordManager: () -> Unit = {},
|
||||||
onSaveToPasswordManagerComplete: (CredentialManagerResult) -> Unit = {},
|
onSaveToPasswordManagerComplete: (CredentialManagerResult) -> Unit = {},
|
||||||
|
onSaveStateCleared: () -> Unit = {},
|
||||||
onGoToPasswordManagerSettingsClick: () -> Unit = {},
|
onGoToPasswordManagerSettingsClick: () -> Unit = {},
|
||||||
mode: MessageBackupsKeyRecordMode = MessageBackupsKeyRecordMode.Next(onNextClick = {}),
|
mode: MessageBackupsKeyRecordMode = MessageBackupsKeyRecordMode.Next(onNextClick = {}),
|
||||||
notifyKeyIsSameAsOnDeviceBackupKey: Boolean = false
|
notifyKeyIsSameAsOnDeviceBackupKey: Boolean = false
|
||||||
) {
|
) {
|
||||||
TemporaryScreenshotSecurity.bind()
|
TemporaryScreenshotSecurity.bind()
|
||||||
|
|
||||||
|
val context = LocalContext.current
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
val snackbarHostState = remember { SnackbarHostState() }
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
val backupKeyString = remember(backupKey) {
|
val backupKeyString = remember(backupKey) {
|
||||||
backupKey.chunked(4).joinToString(" ")
|
backupKey.chunked(4).joinToString(" ")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mode is MessageBackupsKeyRecordMode.Next) {
|
val showAsPasskey = mode is MessageBackupsKeyRecordMode.Passkey
|
||||||
|
var showExpandedPasskey by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
if (mode is MessageBackupsKeyRecordMode.Next || mode is MessageBackupsKeyRecordMode.Passkey) {
|
||||||
RecordScreenBackHandler()
|
RecordScreenBackHandler()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -183,6 +211,41 @@ fun MessageBackupsKeyRecordScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var displayKeyVerificationError by remember { mutableStateOf(false) }
|
||||||
|
if (displayKeyVerificationError) {
|
||||||
|
ConfirmationFailureDialog(mode) {
|
||||||
|
displayKeyVerificationError = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var displayConfirmKey by remember { mutableStateOf(false) }
|
||||||
|
if (displayConfirmKey) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val credentialId = stringResource(R.string.MessageBackupsKeyRecordScreen__backup_key_password_manager_id)
|
||||||
|
val successMessage = stringResource(R.string.MessageBackupsKeyRecordScreen__recover_key_confirmed)
|
||||||
|
ModalBottomSheet(
|
||||||
|
sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true),
|
||||||
|
containerColor = SignalTheme.colors.colorSurface1,
|
||||||
|
onDismissRequest = { displayConfirmKey = false }
|
||||||
|
) {
|
||||||
|
ConfirmRecoveryKeySheet(
|
||||||
|
onConfirm = {
|
||||||
|
coroutineScope.launch {
|
||||||
|
val retrieved = getKeyFromCredentialManager(context, credentialId)
|
||||||
|
if (retrieved == backupKey) {
|
||||||
|
Toast.makeText(context, successMessage, Toast.LENGTH_SHORT).show()
|
||||||
|
(mode as? MessageBackupsKeyRecordMode.Passkey)?.onSaveSuccessful()
|
||||||
|
} else {
|
||||||
|
displayKeyVerificationError = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
displayConfirmKey = false
|
||||||
|
},
|
||||||
|
onSeeAgain = { displayConfirmKey = false }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Scaffolds.Settings(
|
Scaffolds.Settings(
|
||||||
title = "",
|
title = "",
|
||||||
navigationIcon = SignalIcons.ArrowStart.imageVector,
|
navigationIcon = SignalIcons.ArrowStart.imageVector,
|
||||||
@ -216,7 +279,11 @@ fun MessageBackupsKeyRecordScreen(
|
|||||||
|
|
||||||
item {
|
item {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.MessageBackupsKeyRecordScreen__record_your_backup_key),
|
text = if (showAsPasskey) {
|
||||||
|
stringResource(R.string.MessageBackupsKeyRecordScreen__save_your_recovery_key)
|
||||||
|
} else {
|
||||||
|
stringResource(R.string.MessageBackupsKeyRecordScreen__record_your_backup_key)
|
||||||
|
},
|
||||||
style = MaterialTheme.typography.headlineMedium,
|
style = MaterialTheme.typography.headlineMedium,
|
||||||
modifier = Modifier.padding(top = 16.dp)
|
modifier = Modifier.padding(top = 16.dp)
|
||||||
)
|
)
|
||||||
@ -225,6 +292,8 @@ fun MessageBackupsKeyRecordScreen(
|
|||||||
item {
|
item {
|
||||||
val text = if (notifyKeyIsSameAsOnDeviceBackupKey) {
|
val text = if (notifyKeyIsSameAsOnDeviceBackupKey) {
|
||||||
stringResource(R.string.MessageBackupsKeyRecordScreen__this_key_is_the_same_as_your_on_device_recovery_key)
|
stringResource(R.string.MessageBackupsKeyRecordScreen__this_key_is_the_same_as_your_on_device_recovery_key)
|
||||||
|
} else if (showAsPasskey) {
|
||||||
|
stringResource(R.string.MessageBackupsKeyRecordScreen__your_recovery_key)
|
||||||
} else {
|
} else {
|
||||||
stringResource(R.string.MessageBackupsKeyRecordScreen__this_key_is_required_to_recover)
|
stringResource(R.string.MessageBackupsKeyRecordScreen__this_key_is_required_to_recover)
|
||||||
}
|
}
|
||||||
@ -239,47 +308,113 @@ fun MessageBackupsKeyRecordScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
item {
|
item {
|
||||||
Box(
|
AnimatedContent(
|
||||||
modifier = Modifier
|
targetState = showAsPasskey && !showExpandedPasskey,
|
||||||
.padding(top = 24.dp, bottom = 16.dp)
|
transitionSpec = { fadeIn() togetherWith fadeOut() using SizeTransform(clip = false) },
|
||||||
.background(
|
label = "passkey",
|
||||||
color = SignalTheme.colors.colorSurface1,
|
modifier = Modifier.padding(top = 24.dp, bottom = 16.dp)
|
||||||
shape = RoundedCornerShape(10.dp)
|
) { isCollapsed ->
|
||||||
)
|
if (isCollapsed) {
|
||||||
.padding(24.dp)
|
Box(
|
||||||
) {
|
contentAlignment = Alignment.CenterEnd,
|
||||||
Text(
|
modifier = Modifier.background(
|
||||||
text = backupKeyString,
|
color = SignalTheme.colors.colorSurface1,
|
||||||
style = MaterialTheme.typography.bodyLarge
|
shape = RoundedCornerShape(50.dp)
|
||||||
.copy(
|
)
|
||||||
fontSize = 18.sp,
|
) {
|
||||||
fontWeight = FontWeight(400),
|
Text(
|
||||||
letterSpacing = 1.44.sp,
|
text = backupKeyString,
|
||||||
lineHeight = 36.sp,
|
maxLines = 1,
|
||||||
textAlign = TextAlign.Center,
|
style = MaterialTheme.typography.bodyLarge
|
||||||
fontFamily = MonoTypeface.fontFamily()
|
.copy(
|
||||||
|
fontSize = 18.sp,
|
||||||
|
fontWeight = FontWeight(400),
|
||||||
|
letterSpacing = 1.44.sp,
|
||||||
|
lineHeight = 36.sp,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
fontFamily = MonoTypeface.fontFamily()
|
||||||
|
),
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(10.dp)
|
||||||
|
.graphicsLayer { compositingStrategy = CompositingStrategy.Offscreen }
|
||||||
|
.drawWithContent {
|
||||||
|
drawContent()
|
||||||
|
drawRect(
|
||||||
|
brush = Brush.horizontalGradient(
|
||||||
|
0f to Color.Black,
|
||||||
|
0.75f to Color.Transparent
|
||||||
|
),
|
||||||
|
blendMode = BlendMode.DstIn
|
||||||
|
)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
item {
|
Row(
|
||||||
Buttons.Small(
|
modifier = Modifier
|
||||||
onClick = {
|
.background(
|
||||||
if (mode is MessageBackupsKeyRecordMode.CreateNewKey) {
|
color = SignalTheme.colors.colorSurface1,
|
||||||
displayRecoveryKeyCopyWarning = true
|
shape = RoundedCornerShape(50.dp)
|
||||||
} else {
|
)
|
||||||
onCopyToClipboardClick(backupKeyString)
|
.padding(horizontal = 12.dp)
|
||||||
|
.clickable(onClick = { showExpandedPasskey = true }),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = ImageVector.vectorResource(R.drawable.symbol_tap_20),
|
||||||
|
contentDescription = stringResource(R.string.MessageBackupsKeyRecordScreen__see_full_key)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.MessageBackupsKeyRecordScreen__see_full_key),
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
modifier = Modifier.padding(start = 4.dp, end = 12.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(
|
||||||
|
color = SignalTheme.colors.colorSurface1,
|
||||||
|
shape = RoundedCornerShape(10.dp)
|
||||||
|
)
|
||||||
|
.padding(24.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = backupKeyString,
|
||||||
|
style = MaterialTheme.typography.bodyLarge
|
||||||
|
.copy(
|
||||||
|
fontSize = 18.sp,
|
||||||
|
fontWeight = FontWeight(400),
|
||||||
|
letterSpacing = 1.44.sp,
|
||||||
|
lineHeight = 36.sp,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
fontFamily = MonoTypeface.fontFamily()
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.MessageBackupsKeyRecordScreen__copy_to_clipboard)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (AndroidCredentialRepository.isCredentialManagerSupported) {
|
if (!showAsPasskey || showExpandedPasskey) {
|
||||||
|
item {
|
||||||
|
Buttons.Small(
|
||||||
|
onClick = {
|
||||||
|
if (mode is MessageBackupsKeyRecordMode.CreateNewKey) {
|
||||||
|
displayRecoveryKeyCopyWarning = true
|
||||||
|
} else {
|
||||||
|
onCopyToClipboardClick(backupKeyString)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.MessageBackupsKeyRecordScreen__copy_to_clipboard)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!showAsPasskey && AndroidCredentialRepository.isCredentialManagerSupported(context)) {
|
||||||
item {
|
item {
|
||||||
Buttons.Small(
|
Buttons.Small(
|
||||||
onClick = { onRequestSaveToPasswordManager() }
|
onClick = { onRequestSaveToPasswordManager() }
|
||||||
@ -300,6 +435,10 @@ fun MessageBackupsKeyRecordScreen(
|
|||||||
is MessageBackupsKeyRecordMode.CreateNewKey -> {
|
is MessageBackupsKeyRecordMode.CreateNewKey -> {
|
||||||
CreateNewKeyButton(mode)
|
CreateNewKeyButton(mode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is MessageBackupsKeyRecordMode.Passkey -> {
|
||||||
|
SaveButtons(mode)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -326,7 +465,12 @@ fun MessageBackupsKeyRecordScreen(
|
|||||||
is BackupKeySaveState.Success -> {
|
is BackupKeySaveState.Success -> {
|
||||||
val snackbarMessage = stringResource(R.string.MessageBackupsKeyRecordScreen__save_to_password_manager_success)
|
val snackbarMessage = stringResource(R.string.MessageBackupsKeyRecordScreen__save_to_password_manager_success)
|
||||||
LaunchedEffect(keySaveState) {
|
LaunchedEffect(keySaveState) {
|
||||||
snackbarHostState.showSnackbar(snackbarMessage)
|
if (showAsPasskey) {
|
||||||
|
displayConfirmKey = true
|
||||||
|
} else {
|
||||||
|
snackbarHostState.showSnackbar(snackbarMessage)
|
||||||
|
}
|
||||||
|
onSaveStateCleared()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -343,6 +487,24 @@ fun MessageBackupsKeyRecordScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SaveButtons(mode: MessageBackupsKeyRecordMode.Passkey) {
|
||||||
|
Buttons.LargeTonal(
|
||||||
|
onClick = mode.onSaveToPasswordManager
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(R.string.MessageBackupsKeyRecordScreen__save_to_password_manager))
|
||||||
|
}
|
||||||
|
|
||||||
|
TextButton(
|
||||||
|
onClick = mode.onSaveManually,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(vertical = 24.dp)
|
||||||
|
.horizontalGutters()
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(R.string.MessageBackupsKeyRecordScreen__save_key_manually))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun NextButton(onNextClick: () -> Unit) {
|
private fun NextButton(onNextClick: () -> Unit) {
|
||||||
Box(
|
Box(
|
||||||
@ -578,6 +740,26 @@ private fun KeyLimitExceededDialog(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ConfirmationFailureDialog(mode: MessageBackupsKeyRecordMode, onDismiss: () -> Unit) {
|
||||||
|
Dialogs.AdvancedAlertDialog(
|
||||||
|
title = stringResource(R.string.MessageBackupsKeyRecordScreen__recover_key_error),
|
||||||
|
body = stringResource(R.string.MessageBackupsKeyRecordScreen__recover_key_error_body),
|
||||||
|
positive = stringResource(R.string.MessageBackupsKeyRecordScreen__save_to_password_manager),
|
||||||
|
onPositive = {
|
||||||
|
(mode as? MessageBackupsKeyRecordMode.Passkey)?.onSaveToPasswordManager()
|
||||||
|
onDismiss()
|
||||||
|
},
|
||||||
|
neutral = stringResource(R.string.MessageBackupsKeyRecordScreen__save_key_manually),
|
||||||
|
onNeutral = {
|
||||||
|
(mode as? MessageBackupsKeyRecordMode.Passkey)?.onSaveManually()
|
||||||
|
onDismiss()
|
||||||
|
},
|
||||||
|
negative = stringResource(android.R.string.cancel),
|
||||||
|
onNegative = onDismiss
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun saveKeyToCredentialManager(
|
private suspend fun saveKeyToCredentialManager(
|
||||||
@UiContext activityContext: Context,
|
@UiContext activityContext: Context,
|
||||||
backupKey: String
|
backupKey: String
|
||||||
@ -589,6 +771,13 @@ private suspend fun saveKeyToCredentialManager(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun getKeyFromCredentialManager(
|
||||||
|
@UiContext activityContext: Context,
|
||||||
|
id: String
|
||||||
|
): String? {
|
||||||
|
return AndroidCredentialRepository.getCredential(activityContext, id)
|
||||||
|
}
|
||||||
|
|
||||||
@DayNightPreviews
|
@DayNightPreviews
|
||||||
@Composable
|
@Composable
|
||||||
private fun MessageBackupsKeyRecordScreenPreview() {
|
private fun MessageBackupsKeyRecordScreenPreview() {
|
||||||
@ -621,6 +810,23 @@ private fun MessageBackupsKeyRecordScreenSameAsOnDeviceKeyPreview() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@DayNightPreviews
|
||||||
|
@Composable
|
||||||
|
private fun MessageBackupsKeySaveScreenPreview() {
|
||||||
|
Previews.Preview {
|
||||||
|
MessageBackupsKeyRecordScreen(
|
||||||
|
backupKey = (0 until 63).map { (('A'..'Z') + ('0'..'9')).random() }.joinToString("") + "0",
|
||||||
|
keySaveState = null,
|
||||||
|
canOpenPasswordManagerSettings = true,
|
||||||
|
mode = MessageBackupsKeyRecordMode.Passkey(
|
||||||
|
onSaveToPasswordManager = {},
|
||||||
|
onSaveManually = {},
|
||||||
|
onSaveSuccessful = {}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@DayNightPreviews
|
@DayNightPreviews
|
||||||
@Composable
|
@Composable
|
||||||
private fun SaveKeyConfirmationDialogPreview() {
|
private fun SaveKeyConfirmationDialogPreview() {
|
||||||
|
|||||||
@ -15,6 +15,7 @@ enum class MessageBackupsStage(
|
|||||||
EDUCATION(route = Route.EDUCATION),
|
EDUCATION(route = Route.EDUCATION),
|
||||||
BACKUP_KEY_EDUCATION(route = Route.BACKUP_KEY_EDUCATION),
|
BACKUP_KEY_EDUCATION(route = Route.BACKUP_KEY_EDUCATION),
|
||||||
BACKUP_KEY_RECORD(route = Route.BACKUP_KEY_RECORD),
|
BACKUP_KEY_RECORD(route = Route.BACKUP_KEY_RECORD),
|
||||||
|
BACKUP_KEY_RECORD_MANUALLY(route = Route.BACKUP_KEY_RECORD_MANUALLY),
|
||||||
BACKUP_KEY_VERIFY(route = Route.BACKUP_KEY_VERIFY),
|
BACKUP_KEY_VERIFY(route = Route.BACKUP_KEY_VERIFY),
|
||||||
TYPE_SELECTION(route = Route.TYPE_SELECTION),
|
TYPE_SELECTION(route = Route.TYPE_SELECTION),
|
||||||
CREATING_IN_APP_PAYMENT(route = Route.TYPE_SELECTION),
|
CREATING_IN_APP_PAYMENT(route = Route.TYPE_SELECTION),
|
||||||
@ -32,6 +33,7 @@ enum class MessageBackupsStage(
|
|||||||
EDUCATION,
|
EDUCATION,
|
||||||
BACKUP_KEY_EDUCATION,
|
BACKUP_KEY_EDUCATION,
|
||||||
BACKUP_KEY_RECORD,
|
BACKUP_KEY_RECORD,
|
||||||
|
BACKUP_KEY_RECORD_MANUALLY,
|
||||||
BACKUP_KEY_VERIFY,
|
BACKUP_KEY_VERIFY,
|
||||||
TYPE_SELECTION;
|
TYPE_SELECTION;
|
||||||
|
|
||||||
|
|||||||
@ -61,7 +61,7 @@ class VerifyBackupKeyActivity : PassphraseRequiredActivity() {
|
|||||||
// Matches existing behavior: show a generic "authentication required" toast.
|
// Matches existing behavior: show a generic "authentication required" toast.
|
||||||
Toast.makeText(
|
Toast.makeText(
|
||||||
context,
|
context,
|
||||||
R.string.RemoteBackupsSettingsFragment__authenticatino_required,
|
R.string.RemoteBackupsSettingsFragment__authentication_required,
|
||||||
Toast.LENGTH_SHORT
|
Toast.LENGTH_SHORT
|
||||||
).show()
|
).show()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,8 +7,8 @@ package org.thoughtcrime.securesms.backup.v2.util
|
|||||||
|
|
||||||
import org.signal.archive.proto.ChatStyle
|
import org.signal.archive.proto.ChatStyle
|
||||||
import org.signal.archive.proto.FilePointer
|
import org.signal.archive.proto.FilePointer
|
||||||
|
import org.signal.core.models.database.AttachmentId
|
||||||
import org.signal.core.util.logging.Log
|
import org.signal.core.util.logging.Log
|
||||||
import org.thoughtcrime.securesms.attachments.AttachmentId
|
|
||||||
import org.thoughtcrime.securesms.backup.v2.BackupMode
|
import org.thoughtcrime.securesms.backup.v2.BackupMode
|
||||||
import org.thoughtcrime.securesms.backup.v2.ImportState
|
import org.thoughtcrime.securesms.backup.v2.ImportState
|
||||||
import org.thoughtcrime.securesms.conversation.colors.ChatColors
|
import org.thoughtcrime.securesms.conversation.colors.ChatColors
|
||||||
|
|||||||
@ -34,6 +34,7 @@ import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType
|
|||||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.InAppPaymentCheckoutDelegate
|
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.InAppPaymentCheckoutDelegate
|
||||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||||
|
import org.thoughtcrime.securesms.util.storage.AndroidCredentialRepository
|
||||||
import org.thoughtcrime.securesms.util.viewModel
|
import org.thoughtcrime.securesms.util.viewModel
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -61,6 +62,7 @@ abstract class UpgradeToPaidTierBottomSheet : ComposeBottomSheetDialogFragment()
|
|||||||
MessageBackupsFlowViewModel(
|
MessageBackupsFlowViewModel(
|
||||||
initialTierSelection = MessageBackupTier.PAID,
|
initialTierSelection = MessageBackupTier.PAID,
|
||||||
googlePlayApiAvailability = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(requireContext()),
|
googlePlayApiAvailability = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(requireContext()),
|
||||||
|
isCredentialManagerSupported = AndroidCredentialRepository.isCredentialManagerSupported(requireContext()),
|
||||||
startScreen = MessageBackupsStage.TYPE_SELECTION
|
startScreen = MessageBackupsStage.TYPE_SELECTION
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -221,7 +221,7 @@ fun CallLinkDetailsScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.callLink.credentials?.adminPassBytes != null) {
|
if (state.callLink.canModify) {
|
||||||
item {
|
item {
|
||||||
Rows.TextRow(
|
Rows.TextRow(
|
||||||
text = stringResource(
|
text = stringResource(
|
||||||
@ -273,13 +273,15 @@ fun CallLinkDetailsScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
item {
|
if (state.callLink.canModify) {
|
||||||
Rows.TextRow(
|
item {
|
||||||
text = stringResource(id = R.string.CallLinkDetailsFragment__delete_call_link),
|
Rows.TextRow(
|
||||||
icon = SignalIcons.Trash.imageVector,
|
text = stringResource(id = R.string.CallLinkDetailsFragment__delete_call_link),
|
||||||
foregroundTint = MaterialTheme.colorScheme.error,
|
icon = SignalIcons.Trash.imageVector,
|
||||||
onClick = callback::onDeleteClicked
|
foregroundTint = MaterialTheme.colorScheme.error,
|
||||||
)
|
onClick = callback::onDeleteClicked
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,85 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2026 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.thoughtcrime.securesms.chats
|
||||||
|
|
||||||
|
import androidx.compose.runtime.mutableStateListOf
|
||||||
|
import androidx.compose.runtime.saveable.Saver
|
||||||
|
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||||
|
import androidx.lifecycle.SavedStateHandle
|
||||||
|
import androidx.lifecycle.viewmodel.compose.SavedStateHandleSaveableApi
|
||||||
|
import androidx.lifecycle.viewmodel.compose.saveable
|
||||||
|
import org.thoughtcrime.securesms.main.MainNavigationDetailLocation
|
||||||
|
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controls the navigation stack used by the chats screen.
|
||||||
|
*/
|
||||||
|
@OptIn(SavedStateHandleSaveableApi::class)
|
||||||
|
class ChatsBackStack(savedStateHandle: SavedStateHandle) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val KEY = "chats_back_stack"
|
||||||
|
|
||||||
|
val saver: Saver<SnapshotStateList<MainNavigationDetailLocation>, ArrayList<MainNavigationDetailLocation>> = Saver(
|
||||||
|
save = { ArrayList(it) },
|
||||||
|
restore = { mutableStateListOf(*it.toTypedArray()) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val entries: SnapshotStateList<MainNavigationDetailLocation> = savedStateHandle.saveable(
|
||||||
|
key = KEY,
|
||||||
|
saver = saver
|
||||||
|
) {
|
||||||
|
mutableStateListOf(MainNavigationDetailLocation.Empty)
|
||||||
|
}
|
||||||
|
|
||||||
|
val activeRecipientId: RecipientId?
|
||||||
|
get() = entries.asReversed().firstNotNullOfOrNull {
|
||||||
|
when (it) {
|
||||||
|
is MainNavigationDetailLocation.Conversation -> it.conversationArgs.recipientId
|
||||||
|
is MainNavigationDetailLocation.Chats -> it.controllerKey
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val isEmpty: Boolean
|
||||||
|
get() = entries.singleOrNull() is MainNavigationDetailLocation.Empty
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pushes an entry onto the stack.
|
||||||
|
*/
|
||||||
|
fun push(location: MainNavigationDetailLocation) {
|
||||||
|
when (location) {
|
||||||
|
is MainNavigationDetailLocation.Empty, entries.lastOrNull() -> Unit
|
||||||
|
|
||||||
|
is MainNavigationDetailLocation.Conversation -> {
|
||||||
|
entries.removeAll { it !is MainNavigationDetailLocation.Empty }
|
||||||
|
entries.add(location)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> entries.add(location)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pops the top entry off the stack. Returns true if something was popped, false if the stack is already at its root.
|
||||||
|
*/
|
||||||
|
fun pop(): Boolean {
|
||||||
|
if (entries.size <= 1) return false
|
||||||
|
entries.removeAt(entries.lastIndex)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets the stack to its base empty state.
|
||||||
|
*/
|
||||||
|
fun reset() {
|
||||||
|
entries.removeAll { it !is MainNavigationDetailLocation.Empty }
|
||||||
|
if (entries.isEmpty()) {
|
||||||
|
entries.add(MainNavigationDetailLocation.Empty)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,161 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2026 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.thoughtcrime.securesms.chats
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||||
|
import androidx.compose.foundation.layout.statusBarsPadding
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.key
|
||||||
|
import androidx.compose.runtime.produceState
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.fragment.compose.AndroidFragment
|
||||||
|
import androidx.fragment.compose.rememberFragmentState
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
|
import androidx.navigation3.runtime.EntryProviderScope
|
||||||
|
import androidx.navigation3.runtime.NavKey
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.signal.core.ui.navigation.TransitionSpecs
|
||||||
|
import org.thoughtcrime.securesms.MainNavigator
|
||||||
|
import org.thoughtcrime.securesms.components.settings.conversation.ConversationSettingsNavHostFragment
|
||||||
|
import org.thoughtcrime.securesms.compose.FragmentBackHandler
|
||||||
|
import org.thoughtcrime.securesms.compose.FragmentBackPressedState
|
||||||
|
import org.thoughtcrime.securesms.conversation.ConversationIntents
|
||||||
|
import org.thoughtcrime.securesms.conversation.v2.ConversationFragment
|
||||||
|
import org.thoughtcrime.securesms.main.EmptyDetailScreen
|
||||||
|
import org.thoughtcrime.securesms.main.MainNavigationDetailLocation
|
||||||
|
import org.thoughtcrime.securesms.messagedetails.MessageDetailsFragment
|
||||||
|
|
||||||
|
fun EntryProviderScope<NavKey>.chatsNavEntries(
|
||||||
|
transitionState: ConversationTransitionState
|
||||||
|
) {
|
||||||
|
entry<MainNavigationDetailLocation.Empty> {
|
||||||
|
NoConvoSelectedEntry()
|
||||||
|
}
|
||||||
|
|
||||||
|
entry<MainNavigationDetailLocation.Conversation>(
|
||||||
|
// disable slide animation - it's unnecessary in split pane mode and is handled by ConversationLoadingMask for single pane mode.
|
||||||
|
metadata = TransitionSpecs.None.metadata
|
||||||
|
) { route ->
|
||||||
|
ConversationEntry(route, transitionState)
|
||||||
|
}
|
||||||
|
|
||||||
|
entry<MainNavigationDetailLocation.Chats.MessageDetails> { route ->
|
||||||
|
MessageDetailsEntry(route)
|
||||||
|
}
|
||||||
|
|
||||||
|
entry<MainNavigationDetailLocation.Chats.ConversationSettings> { route ->
|
||||||
|
ConversationSettingsEntry(route)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun NoConvoSelectedEntry() {
|
||||||
|
EmptyDetailScreen()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ConversationEntry(
|
||||||
|
route: MainNavigationDetailLocation.Conversation,
|
||||||
|
transitionState: ConversationTransitionState
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val navigatorProvider = context as? MainNavigator.NavigatorProvider
|
||||||
|
val fragmentState = key(route) { rememberFragmentState() }
|
||||||
|
val arguments = requireNotNull(ConversationIntents.createBuilderSync(context, route.conversationArgs).build().extras) {
|
||||||
|
"Handed null Conversation intent arguments."
|
||||||
|
}
|
||||||
|
|
||||||
|
val fragmentContentReady = remember { MutableStateFlow(false) }
|
||||||
|
val backPressedState = remember { FragmentBackPressedState() }
|
||||||
|
FragmentBackHandler(backPressedState)
|
||||||
|
|
||||||
|
ConversationLoadingMask(
|
||||||
|
transitionState = transitionState,
|
||||||
|
contentReady = fragmentContentReady,
|
||||||
|
onFirstRender = { navigatorProvider?.onFirstRender() }
|
||||||
|
) { modifier ->
|
||||||
|
AndroidFragment(
|
||||||
|
clazz = ConversationFragment::class.java,
|
||||||
|
fragmentState = fragmentState,
|
||||||
|
arguments = arguments,
|
||||||
|
modifier = modifier
|
||||||
|
.background(MaterialTheme.colorScheme.background)
|
||||||
|
.fillMaxSize()
|
||||||
|
) { fragment ->
|
||||||
|
backPressedState.attach(fragment)
|
||||||
|
|
||||||
|
fragment.viewLifecycleOwner.lifecycleScope.launch {
|
||||||
|
fragment.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
|
fragment.didFirstFrameRender.collectLatest { fragmentContentReady.value = it }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun MessageDetailsEntry(route: MainNavigationDetailLocation.Chats.MessageDetails) {
|
||||||
|
val navigatorProvider = LocalContext.current as? MainNavigator.NavigatorProvider
|
||||||
|
val fragmentState = key(route) { rememberFragmentState() }
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
navigatorProvider?.onFirstRender()
|
||||||
|
}
|
||||||
|
|
||||||
|
AndroidFragment(
|
||||||
|
clazz = MessageDetailsFragment::class.java,
|
||||||
|
fragmentState = fragmentState,
|
||||||
|
arguments = MessageDetailsFragment.args(route.recipientId, route.messageId),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(MaterialTheme.colorScheme.background)
|
||||||
|
.statusBarsPadding()
|
||||||
|
.navigationBarsPadding()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ConversationSettingsEntry(route: MainNavigationDetailLocation.Chats.ConversationSettings) {
|
||||||
|
val navigatorProvider = LocalContext.current as? MainNavigator.NavigatorProvider
|
||||||
|
val fragmentState = key(route) { rememberFragmentState() }
|
||||||
|
val arguments: Bundle? by produceState(null, route.recipientId) {
|
||||||
|
value = ConversationSettingsNavHostFragment.createArgs(route.recipientId)
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
navigatorProvider?.onFirstRender()
|
||||||
|
}
|
||||||
|
|
||||||
|
arguments?.let { args ->
|
||||||
|
val backPressedState = remember { FragmentBackPressedState() }
|
||||||
|
FragmentBackHandler(backPressedState)
|
||||||
|
|
||||||
|
AndroidFragment(
|
||||||
|
clazz = ConversationSettingsNavHostFragment::class.java,
|
||||||
|
fragmentState = fragmentState,
|
||||||
|
arguments = args,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(MaterialTheme.colorScheme.background)
|
||||||
|
.statusBarsPadding()
|
||||||
|
.navigationBarsPadding()
|
||||||
|
) { fragment ->
|
||||||
|
backPressedState.attach(fragment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,128 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2026 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.thoughtcrime.securesms.chats
|
||||||
|
|
||||||
|
import androidx.compose.animation.core.Transition
|
||||||
|
import androidx.compose.animation.core.animateDp
|
||||||
|
import androidx.compose.animation.core.animateFloat
|
||||||
|
import androidx.compose.animation.core.updateTransition
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.withTimeoutOrNull
|
||||||
|
import org.thoughtcrime.securesms.window.AppScaffoldAnimationDefaults
|
||||||
|
import org.thoughtcrime.securesms.window.AppScaffoldAnimationState
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wraps [content] with an animation that crossfades a snapshotted chat list bitmap over the conversation fragment while it loads its first frame.
|
||||||
|
*
|
||||||
|
* @param contentReady emits when the fragment's first frame has been rendered.
|
||||||
|
* @param onFirstRender signals that this composable has content ready to display, so the parent activity can proceed with its first draw.
|
||||||
|
* @param content will be animated in as the overlay fades out.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun ConversationLoadingMask(
|
||||||
|
transitionState: ConversationTransitionState,
|
||||||
|
contentReady: StateFlow<Boolean>,
|
||||||
|
onFirstRender: () -> Unit,
|
||||||
|
content: @Composable (chatModifier: Modifier) -> Unit
|
||||||
|
) {
|
||||||
|
// it can take a long time to load content, so we use a "fake" chat list image to delay displaying the fragment
|
||||||
|
// and prevent pop-in. When there's no bitmap (e.g. returning from a sub-route), skip the animation.
|
||||||
|
var shouldDisplayFragment by remember {
|
||||||
|
val hasBitmap = transitionState.chatBitmap != null
|
||||||
|
mutableStateOf(!hasBitmap)
|
||||||
|
}
|
||||||
|
val transition: Transition<Boolean> = updateTransition(shouldDisplayFragment)
|
||||||
|
val bitmap = transitionState.chatBitmap
|
||||||
|
|
||||||
|
val fakeChatListAnimationState = transition.fakeChatListAnimationState()
|
||||||
|
val chatAnimationState = transition.chatAnimationState(hasFake = bitmap != null)
|
||||||
|
|
||||||
|
LaunchedEffect(transition.currentState, transition.isRunning) {
|
||||||
|
if (transition.currentState && !transition.isRunning) {
|
||||||
|
transitionState.clearBitmap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(shouldDisplayFragment) {
|
||||||
|
onFirstRender()
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(contentReady) {
|
||||||
|
if (!shouldDisplayFragment) {
|
||||||
|
withTimeoutOrNull(5.seconds) {
|
||||||
|
contentReady.first { it }
|
||||||
|
}
|
||||||
|
shouldDisplayFragment = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val chatModifier = Modifier.graphicsLayer {
|
||||||
|
with(chatAnimationState) { applyChildValues() }
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
|
content(chatModifier)
|
||||||
|
|
||||||
|
if (bitmap != null) {
|
||||||
|
Image(
|
||||||
|
bitmap = bitmap,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier
|
||||||
|
.graphicsLayer {
|
||||||
|
with(fakeChatListAnimationState) { applyChildValues() }
|
||||||
|
}
|
||||||
|
.fillMaxSize()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun Transition<Boolean>.fakeChatListAnimationState(): AppScaffoldAnimationState {
|
||||||
|
val alpha = animateFloat(transitionSpec = { AppScaffoldAnimationDefaults.tween() }) { if (it) 0f else 1f }
|
||||||
|
val offset = animateDp(transitionSpec = { AppScaffoldAnimationDefaults.tween() }) { if (it) (-48).dp else 0.dp }
|
||||||
|
return remember {
|
||||||
|
AppScaffoldAnimationState(
|
||||||
|
offset = offset,
|
||||||
|
alpha = alpha
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun Transition<Boolean>.chatAnimationState(hasFake: Boolean): AppScaffoldAnimationState {
|
||||||
|
val alpha = animateFloat(transitionSpec = { AppScaffoldAnimationDefaults.tween() }) { if (it) 1f else 0f }
|
||||||
|
return if (!hasFake) {
|
||||||
|
remember {
|
||||||
|
AppScaffoldAnimationState(
|
||||||
|
offset = mutableStateOf(0.dp),
|
||||||
|
alpha = alpha
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val offset = animateDp(transitionSpec = { AppScaffoldAnimationDefaults.tween() }) { if (it) 0.dp else 48.dp }
|
||||||
|
remember {
|
||||||
|
AppScaffoldAnimationState(
|
||||||
|
offset = offset,
|
||||||
|
alpha = alpha
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,70 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2026 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.thoughtcrime.securesms.chats
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.Stable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.drawWithContent
|
||||||
|
import androidx.compose.ui.graphics.ImageBitmap
|
||||||
|
import androidx.compose.ui.graphics.layer.GraphicsLayer
|
||||||
|
import androidx.compose.ui.graphics.layer.drawLayer
|
||||||
|
import androidx.compose.ui.graphics.rememberGraphicsLayer
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows the setting of a "fake" bitmap driven by a graphics layer to coordinate delayed animations
|
||||||
|
* in lieu of proper support for postponing enter transitions.
|
||||||
|
*/
|
||||||
|
@Stable
|
||||||
|
class ConversationTransitionState private constructor(
|
||||||
|
val isSplitPane: Boolean,
|
||||||
|
val graphicsLayer: GraphicsLayer
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
@Composable
|
||||||
|
fun remember(isSplitPane: Boolean): ConversationTransitionState {
|
||||||
|
val graphicsLayer = rememberGraphicsLayer()
|
||||||
|
|
||||||
|
return remember(isSplitPane) {
|
||||||
|
ConversationTransitionState(isSplitPane, graphicsLayer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var chatBitmap: ImageBitmap? by mutableStateOf(null)
|
||||||
|
private set
|
||||||
|
|
||||||
|
private var hasWrittenToGraphicsLayer: Boolean by mutableStateOf(false)
|
||||||
|
|
||||||
|
suspend fun writeGraphicsLayerToBitmap() {
|
||||||
|
// toImageBitmap() uses LayerSnapshot which has format compatibility issues on Android 7 and below
|
||||||
|
if (Build.VERSION.SDK_INT >= 26 && !isSplitPane && hasWrittenToGraphicsLayer) {
|
||||||
|
chatBitmap = graphicsLayer.toImageBitmap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun writeContentToGraphicsLayer(): Modifier {
|
||||||
|
if (isSplitPane) return Modifier
|
||||||
|
|
||||||
|
return Modifier.drawWithContent {
|
||||||
|
graphicsLayer.record {
|
||||||
|
this@drawWithContent.drawContent()
|
||||||
|
hasWrittenToGraphicsLayer = true
|
||||||
|
}
|
||||||
|
|
||||||
|
drawLayer(graphicsLayer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearBitmap() {
|
||||||
|
chatBitmap = null
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -20,8 +20,8 @@ data class ViewColorSet(
|
|||||||
) : Parcelable {
|
) : Parcelable {
|
||||||
companion object {
|
companion object {
|
||||||
val PRIMARY = ViewColorSet(
|
val PRIMARY = ViewColorSet(
|
||||||
foreground = ViewColor.ColorResource(CoreUiR.color.signal_colorOnPrimary),
|
foreground = ViewColor.ColorResource(CoreUiR.color.signal_light_colorOnPrimary),
|
||||||
background = ViewColor.ColorResource(CoreUiR.color.signal_colorPrimary)
|
background = ViewColor.ColorResource(CoreUiR.color.signal_light_colorPrimary)
|
||||||
)
|
)
|
||||||
|
|
||||||
fun forCustomColor(@ColorInt customColor: Int): ViewColorSet {
|
fun forCustomColor(@ColorInt customColor: Int): ViewColorSet {
|
||||||
|
|||||||
@ -39,6 +39,7 @@ import org.thoughtcrime.securesms.audio.AudioWaveForms;
|
|||||||
import org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackState;
|
import org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackState;
|
||||||
import org.thoughtcrime.securesms.database.AttachmentTable;
|
import org.thoughtcrime.securesms.database.AttachmentTable;
|
||||||
import org.thoughtcrime.securesms.events.PartProgressEvent;
|
import org.thoughtcrime.securesms.events.PartProgressEvent;
|
||||||
|
import org.thoughtcrime.securesms.jobs.AttachmentBackfill;
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||||
import org.thoughtcrime.securesms.mms.AudioSlide;
|
import org.thoughtcrime.securesms.mms.AudioSlide;
|
||||||
import org.thoughtcrime.securesms.mms.SlideClickListener;
|
import org.thoughtcrime.securesms.mms.SlideClickListener;
|
||||||
@ -82,9 +83,11 @@ public final class AudioView extends FrameLayout {
|
|||||||
private boolean isPlaying;
|
private boolean isPlaying;
|
||||||
private long durationMillis;
|
private long durationMillis;
|
||||||
private AudioSlide audioSlide;
|
private AudioSlide audioSlide;
|
||||||
|
private boolean showControls;
|
||||||
private Callbacks callbacks;
|
private Callbacks callbacks;
|
||||||
|
|
||||||
private Disposable disposable = Disposable.disposed();
|
private Disposable disposable = Disposable.disposed();
|
||||||
|
private Disposable awaitingDisposable = Disposable.disposed();
|
||||||
|
|
||||||
private final Observer<VoiceNotePlaybackState> playbackStateObserver = this::onPlaybackState;
|
private final Observer<VoiceNotePlaybackState> playbackStateObserver = this::onPlaybackState;
|
||||||
|
|
||||||
@ -159,6 +162,12 @@ public final class AudioView extends FrameLayout {
|
|||||||
protected void onAttachedToWindow() {
|
protected void onAttachedToWindow() {
|
||||||
super.onAttachedToWindow();
|
super.onAttachedToWindow();
|
||||||
if (!EventBus.getDefault().isRegistered(this)) EventBus.getDefault().register(this);
|
if (!EventBus.getDefault().isRegistered(this)) EventBus.getDefault().register(this);
|
||||||
|
|
||||||
|
awaitingDisposable = AttachmentBackfill.awaitingChanges()
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(ignored -> {
|
||||||
|
if (audioSlide != null) presentTransferControls(audioSlide, showControls);
|
||||||
|
}, t -> Log.w(TAG, "Error observing backfill awaiting state.", t));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -166,6 +175,7 @@ public final class AudioView extends FrameLayout {
|
|||||||
super.onDetachedFromWindow();
|
super.onDetachedFromWindow();
|
||||||
EventBus.getDefault().unregister(this);
|
EventBus.getDefault().unregister(this);
|
||||||
disposable.dispose();
|
disposable.dispose();
|
||||||
|
awaitingDisposable.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setProgressAndPlayBackgroundTint(@ColorInt int color) {
|
public void setProgressAndPlayBackgroundTint(@ColorInt int color) {
|
||||||
@ -197,27 +207,8 @@ public final class AudioView extends FrameLayout {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showControls && audio.isPendingDownload()) {
|
this.showControls = showControls;
|
||||||
controlToggle.displayQuick(downloadContainer);
|
presentTransferControls(audio, showControls);
|
||||||
seekBar.setEnabled(false);
|
|
||||||
downloadButton.setOnClickListener(new DownloadClickedListener(audio));
|
|
||||||
if (circleProgress != null) {
|
|
||||||
if (circleProgress.isSpinning()) circleProgress.stopSpinning();
|
|
||||||
circleProgress.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
} else if (showControls && audio.getTransferState() == AttachmentTable.TRANSFER_PROGRESS_STARTED) {
|
|
||||||
controlToggle.displayQuick(progressAndPlay);
|
|
||||||
seekBar.setEnabled(false);
|
|
||||||
showPlayButton();
|
|
||||||
if (circleProgress != null) {
|
|
||||||
circleProgress.setVisibility(View.VISIBLE);
|
|
||||||
circleProgress.spin();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
seekBar.setEnabled(true);
|
|
||||||
if (circleProgress != null && circleProgress.isSpinning()) circleProgress.stopSpinning();
|
|
||||||
showPlayButton();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (seekBar instanceof WaveFormSeekBarView) {
|
if (seekBar instanceof WaveFormSeekBarView) {
|
||||||
WaveFormSeekBarView waveFormView = (WaveFormSeekBarView) seekBar;
|
WaveFormSeekBarView waveFormView = (WaveFormSeekBarView) seekBar;
|
||||||
@ -263,6 +254,38 @@ public final class AudioView extends FrameLayout {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void presentTransferControls(@NonNull AudioSlide audio, boolean showControls) {
|
||||||
|
DatabaseAttachment dbAttachment = audio.asAttachment() instanceof DatabaseAttachment ? (DatabaseAttachment) audio.asAttachment() : null;
|
||||||
|
|
||||||
|
if (dbAttachment != null && dbAttachment.transferState == AttachmentTable.TRANSFER_PROGRESS_DONE && AttachmentBackfill.isAwaitingBackfill(dbAttachment.attachmentId)) {
|
||||||
|
AttachmentBackfill.onAttachmentTerminal(dbAttachment.attachmentId, dbAttachment.mmsId);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean awaitingBackfill = dbAttachment != null && AttachmentBackfill.isAwaitingBackfill(dbAttachment.attachmentId);
|
||||||
|
|
||||||
|
if (showControls && audio.isPendingDownload() && !awaitingBackfill) {
|
||||||
|
controlToggle.displayQuick(downloadContainer);
|
||||||
|
seekBar.setEnabled(false);
|
||||||
|
downloadButton.setOnClickListener(new DownloadClickedListener(audio));
|
||||||
|
if (circleProgress != null) {
|
||||||
|
if (circleProgress.isSpinning()) circleProgress.stopSpinning();
|
||||||
|
circleProgress.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
} else if (showControls && (audio.getTransferState() == AttachmentTable.TRANSFER_PROGRESS_STARTED || awaitingBackfill)) {
|
||||||
|
controlToggle.displayQuick(progressAndPlay);
|
||||||
|
seekBar.setEnabled(false);
|
||||||
|
showPlayButton();
|
||||||
|
if (circleProgress != null) {
|
||||||
|
circleProgress.setVisibility(View.VISIBLE);
|
||||||
|
if (!circleProgress.isSpinning()) circleProgress.spin();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
seekBar.setEnabled(true);
|
||||||
|
if (circleProgress != null && circleProgress.isSpinning()) circleProgress.stopSpinning();
|
||||||
|
showPlayButton();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void setDownloadClickListener(@Nullable SlideClickListener listener) {
|
public void setDownloadClickListener(@Nullable SlideClickListener listener) {
|
||||||
this.downloadListener = listener;
|
this.downloadListener = listener;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -28,6 +28,7 @@ import org.thoughtcrime.securesms.R;
|
|||||||
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
|
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
|
||||||
import org.thoughtcrime.securesms.database.AttachmentTable;
|
import org.thoughtcrime.securesms.database.AttachmentTable;
|
||||||
import org.thoughtcrime.securesms.events.PartProgressEvent;
|
import org.thoughtcrime.securesms.events.PartProgressEvent;
|
||||||
|
import org.thoughtcrime.securesms.jobs.AttachmentBackfill;
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||||
import org.thoughtcrime.securesms.mms.Slide;
|
import org.thoughtcrime.securesms.mms.Slide;
|
||||||
import org.thoughtcrime.securesms.mms.SlideClickListener;
|
import org.thoughtcrime.securesms.mms.SlideClickListener;
|
||||||
@ -36,6 +37,9 @@ import org.whispersystems.signalservice.api.util.OptionalUtil;
|
|||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
|
||||||
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||||
|
import io.reactivex.rxjava3.disposables.Disposable;
|
||||||
|
|
||||||
public class DocumentView extends FrameLayout {
|
public class DocumentView extends FrameLayout {
|
||||||
|
|
||||||
private static final String TAG = Log.tag(DocumentView.class);
|
private static final String TAG = Log.tag(DocumentView.class);
|
||||||
@ -55,6 +59,9 @@ public class DocumentView extends FrameLayout {
|
|||||||
private @Nullable SlidesClickedListener cancelTransferClickListener;
|
private @Nullable SlidesClickedListener cancelTransferClickListener;
|
||||||
private @Nullable SlidesClickedListener resendTransferClickListener;
|
private @Nullable SlidesClickedListener resendTransferClickListener;
|
||||||
private @Nullable Slide documentSlide;
|
private @Nullable Slide documentSlide;
|
||||||
|
private boolean showControls;
|
||||||
|
|
||||||
|
private Disposable awaitingDisposable = Disposable.disposed();
|
||||||
|
|
||||||
public DocumentView(@NonNull Context context) {
|
public DocumentView(@NonNull Context context) {
|
||||||
this(context, null);
|
this(context, null);
|
||||||
@ -98,12 +105,19 @@ public class DocumentView extends FrameLayout {
|
|||||||
if (!EventBus.getDefault().isRegistered(this)) {
|
if (!EventBus.getDefault().isRegistered(this)) {
|
||||||
EventBus.getDefault().register(this);
|
EventBus.getDefault().register(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
awaitingDisposable = AttachmentBackfill.awaitingChanges()
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(ignored -> {
|
||||||
|
if (documentSlide != null) presentTransferControls(documentSlide, showControls);
|
||||||
|
}, t -> Log.w(TAG, "Error observing backfill awaiting state.", t));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onDetachedFromWindow() {
|
protected void onDetachedFromWindow() {
|
||||||
super.onDetachedFromWindow();
|
super.onDetachedFromWindow();
|
||||||
EventBus.getDefault().unregister(this);
|
EventBus.getDefault().unregister(this);
|
||||||
|
awaitingDisposable.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setDownloadClickListener(@Nullable SlideClickListener listener) {
|
public void setDownloadClickListener(@Nullable SlideClickListener listener) {
|
||||||
@ -126,22 +140,8 @@ public class DocumentView extends FrameLayout {
|
|||||||
final boolean showControls,
|
final boolean showControls,
|
||||||
final boolean showSingleLineFilename)
|
final boolean showSingleLineFilename)
|
||||||
{
|
{
|
||||||
if (showControls && documentSlide.getTransferState() == AttachmentTable.TRANSFER_PROGRESS_STARTED) {
|
this.showControls = showControls;
|
||||||
controlToggle.displayQuick(stopUploadButton);
|
presentTransferControls(documentSlide, showControls);
|
||||||
downloadProgress.spin();
|
|
||||||
stopUploadButton.setOnClickListener(new CancelTransferListener(documentSlide));
|
|
||||||
} else if (showControls && documentSlide.getUri() != null && documentSlide.isPendingDownload()) {
|
|
||||||
controlToggle.displayQuick(uploadButton);
|
|
||||||
uploadButton.setOnClickListener(new ResendTransferClickListener(documentSlide));
|
|
||||||
if (downloadProgress.isSpinning()) downloadProgress.stopSpinning();
|
|
||||||
} else if (showControls && documentSlide.getUri() == null && documentSlide.isPendingDownload()) {
|
|
||||||
controlToggle.displayQuick(downloadButton);
|
|
||||||
downloadButton.setOnClickListener(new DownloadClickedListener(documentSlide));
|
|
||||||
if (downloadProgress.isSpinning()) downloadProgress.stopSpinning();
|
|
||||||
} else {
|
|
||||||
controlToggle.displayQuick(iconContainer);
|
|
||||||
if (downloadProgress.isSpinning()) downloadProgress.stopSpinning();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.documentSlide = documentSlide;
|
this.documentSlide = documentSlide;
|
||||||
|
|
||||||
@ -172,6 +172,33 @@ public class DocumentView extends FrameLayout {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void presentTransferControls(@NonNull Slide documentSlide, boolean showControls) {
|
||||||
|
DatabaseAttachment dbAttachment = documentSlide.asAttachment() instanceof DatabaseAttachment ? (DatabaseAttachment) documentSlide.asAttachment() : null;
|
||||||
|
|
||||||
|
if (dbAttachment != null && dbAttachment.transferState == AttachmentTable.TRANSFER_PROGRESS_DONE && AttachmentBackfill.isAwaitingBackfill(dbAttachment.attachmentId)) {
|
||||||
|
AttachmentBackfill.onAttachmentTerminal(dbAttachment.attachmentId, dbAttachment.mmsId);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean awaitingBackfill = dbAttachment != null && AttachmentBackfill.isAwaitingBackfill(dbAttachment.attachmentId);
|
||||||
|
|
||||||
|
if (showControls && (documentSlide.getTransferState() == AttachmentTable.TRANSFER_PROGRESS_STARTED || awaitingBackfill)) {
|
||||||
|
controlToggle.displayQuick(stopUploadButton);
|
||||||
|
if (!downloadProgress.isSpinning()) downloadProgress.spin();
|
||||||
|
stopUploadButton.setOnClickListener(awaitingBackfill ? null : new CancelTransferListener(documentSlide));
|
||||||
|
} else if (showControls && documentSlide.getUri() != null && documentSlide.isPendingDownload()) {
|
||||||
|
controlToggle.displayQuick(uploadButton);
|
||||||
|
uploadButton.setOnClickListener(new ResendTransferClickListener(documentSlide));
|
||||||
|
if (downloadProgress.isSpinning()) downloadProgress.stopSpinning();
|
||||||
|
} else if (showControls && documentSlide.getUri() == null && documentSlide.isPendingDownload()) {
|
||||||
|
controlToggle.displayQuick(downloadButton);
|
||||||
|
downloadButton.setOnClickListener(new DownloadClickedListener(documentSlide));
|
||||||
|
if (downloadProgress.isSpinning()) downloadProgress.stopSpinning();
|
||||||
|
} else {
|
||||||
|
controlToggle.displayQuick(iconContainer);
|
||||||
|
if (downloadProgress.isSpinning()) downloadProgress.stopSpinning();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setFocusable(boolean focusable) {
|
public void setFocusable(boolean focusable) {
|
||||||
super.setFocusable(focusable);
|
super.setFocusable(focusable);
|
||||||
|
|||||||
@ -27,7 +27,7 @@ import org.thoughtcrime.securesms.components.subsampling.AttachmentRegionDecoder
|
|||||||
import org.signal.glide.decryptableuri.DecryptableUri;
|
import org.signal.glide.decryptableuri.DecryptableUri;
|
||||||
import org.thoughtcrime.securesms.mms.PartAuthority;
|
import org.thoughtcrime.securesms.mms.PartAuthority;
|
||||||
import org.thoughtcrime.securesms.util.ActionRequestListener;
|
import org.thoughtcrime.securesms.util.ActionRequestListener;
|
||||||
import org.thoughtcrime.securesms.util.BitmapUtil;
|
import org.signal.core.util.bitmaps.BitmapUtil;
|
||||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|||||||
@ -7,8 +7,8 @@ import io.reactivex.rxjava3.core.Single
|
|||||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||||
import org.signal.core.util.JsonUtils
|
import org.signal.core.util.JsonUtils
|
||||||
import org.signal.network.NetworkResult
|
import org.signal.network.NetworkResult
|
||||||
|
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||||
import org.thoughtcrime.securesms.net.SignalNetwork
|
import org.thoughtcrime.securesms.net.SignalNetwork
|
||||||
import org.thoughtcrime.securesms.providers.BlobProvider
|
|
||||||
|
|
||||||
class ExportAccountDataRepository {
|
class ExportAccountDataRepository {
|
||||||
|
|
||||||
@ -44,7 +44,7 @@ class ExportAccountDataRepository {
|
|||||||
tree["text"].asText()
|
tree["text"].asText()
|
||||||
}
|
}
|
||||||
|
|
||||||
val uri = BlobProvider.getInstance()
|
val uri = AppDependencies.blobs
|
||||||
.forData(dataStr.encodeToByteArray())
|
.forData(dataStr.encodeToByteArray())
|
||||||
.withMimeType(mimeType)
|
.withMimeType(mimeType)
|
||||||
.withFileName(fileName)
|
.withFileName(fileName)
|
||||||
|
|||||||
@ -27,6 +27,9 @@ interface BackupKeyCredentialManagerHandler {
|
|||||||
/** Called when the user confirms they want to save the backup key to the password manager. */
|
/** Called when the user confirms they want to save the backup key to the password manager. */
|
||||||
fun onBackupKeySaveConfirmed() = updateBackupKeySaveState(BackupKeySaveState.AwaitingCredentialManager(isRetry = false))
|
fun onBackupKeySaveConfirmed() = updateBackupKeySaveState(BackupKeySaveState.AwaitingCredentialManager(isRetry = false))
|
||||||
|
|
||||||
|
/** Called to clear the key save state. */
|
||||||
|
fun onBackupKeySaveStateCleared() = updateBackupKeySaveState(null)
|
||||||
|
|
||||||
/** Handles the password manager save operation response. */
|
/** Handles the password manager save operation response. */
|
||||||
fun onBackupKeySaveCompleted(result: CredentialManagerResult) {
|
fun onBackupKeySaveCompleted(result: CredentialManagerResult) {
|
||||||
when (result) {
|
when (result) {
|
||||||
|
|||||||
@ -368,7 +368,7 @@ private fun RemoteBackupsSettingsContent(
|
|||||||
promptTitle = stringResource(R.string.RemoteBackupsSettingsFragment__unlock_to_view_backup_key),
|
promptTitle = stringResource(R.string.RemoteBackupsSettingsFragment__unlock_to_view_backup_key),
|
||||||
educationSheetMessage = stringResource(R.string.RemoteBackupsSettingsFragment__to_view_your_key),
|
educationSheetMessage = stringResource(R.string.RemoteBackupsSettingsFragment__to_view_your_key),
|
||||||
onAuthenticationFailed = {
|
onAuthenticationFailed = {
|
||||||
Toast.makeText(context, R.string.RemoteBackupsSettingsFragment__authenticatino_required, Toast.LENGTH_SHORT).show()
|
Toast.makeText(context, R.string.RemoteBackupsSettingsFragment__authentication_required, Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -25,9 +25,9 @@ import org.signal.core.ui.compose.Scaffolds
|
|||||||
import org.signal.core.ui.compose.SignalIcons
|
import org.signal.core.ui.compose.SignalIcons
|
||||||
import org.signal.core.ui.compose.Texts
|
import org.signal.core.ui.compose.Texts
|
||||||
import org.signal.core.util.bytes
|
import org.signal.core.util.bytes
|
||||||
|
import org.signal.mediasend.SentMediaQuality
|
||||||
import org.thoughtcrime.securesms.R
|
import org.thoughtcrime.securesms.R
|
||||||
import org.thoughtcrime.securesms.compose.rememberStatusBarColorNestedScrollModifier
|
import org.thoughtcrime.securesms.compose.rememberStatusBarColorNestedScrollModifier
|
||||||
import org.thoughtcrime.securesms.mms.SentMediaQuality
|
|
||||||
import org.thoughtcrime.securesms.util.AttachmentUtil
|
import org.thoughtcrime.securesms.util.AttachmentUtil
|
||||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||||
import org.thoughtcrime.securesms.webrtc.CallDataMode
|
import org.thoughtcrime.securesms.webrtc.CallDataMode
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
package org.thoughtcrime.securesms.components.settings.app.data
|
package org.thoughtcrime.securesms.components.settings.app.data
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.mms.SentMediaQuality
|
import org.signal.mediasend.SentMediaQuality
|
||||||
import org.thoughtcrime.securesms.webrtc.CallDataMode
|
import org.thoughtcrime.securesms.webrtc.CallDataMode
|
||||||
|
|
||||||
data class DataAndStorageSettingsState(
|
data class DataAndStorageSettingsState(
|
||||||
|
|||||||
@ -6,11 +6,11 @@ import androidx.lifecycle.ViewModelProvider
|
|||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
|
import org.signal.mediasend.SentMediaQuality
|
||||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||||
import org.thoughtcrime.securesms.keyvalue.SettingsValues.ForceWebsocketMode
|
import org.thoughtcrime.securesms.keyvalue.SettingsValues.ForceWebsocketMode
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
import org.thoughtcrime.securesms.messages.IncomingMessageObserver
|
import org.thoughtcrime.securesms.messages.IncomingMessageObserver
|
||||||
import org.thoughtcrime.securesms.mms.SentMediaQuality
|
|
||||||
import org.thoughtcrime.securesms.util.PlayServicesUtil
|
import org.thoughtcrime.securesms.util.PlayServicesUtil
|
||||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||||
import org.thoughtcrime.securesms.webrtc.CallDataMode
|
import org.thoughtcrime.securesms.webrtc.CallDataMode
|
||||||
|
|||||||
@ -27,6 +27,7 @@ import org.signal.archive.stream.EncryptedBackupReader
|
|||||||
import org.signal.archive.stream.EncryptedBackupReader.Companion.MAC_SIZE
|
import org.signal.archive.stream.EncryptedBackupReader.Companion.MAC_SIZE
|
||||||
import org.signal.core.models.ServiceId
|
import org.signal.core.models.ServiceId
|
||||||
import org.signal.core.models.backup.MessageBackupKey
|
import org.signal.core.models.backup.MessageBackupKey
|
||||||
|
import org.signal.core.models.database.AttachmentId
|
||||||
import org.signal.core.util.Hex
|
import org.signal.core.util.Hex
|
||||||
import org.signal.core.util.ThreadUtil
|
import org.signal.core.util.ThreadUtil
|
||||||
import org.signal.core.util.bytes
|
import org.signal.core.util.bytes
|
||||||
@ -39,7 +40,6 @@ import org.signal.core.util.stream.LimitedInputStream
|
|||||||
import org.signal.libsignal.zkgroup.profiles.ProfileKey
|
import org.signal.libsignal.zkgroup.profiles.ProfileKey
|
||||||
import org.signal.network.NetworkResult
|
import org.signal.network.NetworkResult
|
||||||
import org.signal.network.api.SvrBApi
|
import org.signal.network.api.SvrBApi
|
||||||
import org.thoughtcrime.securesms.attachments.AttachmentId
|
|
||||||
import org.thoughtcrime.securesms.attachments.DatabaseAttachment
|
import org.thoughtcrime.securesms.attachments.DatabaseAttachment
|
||||||
import org.thoughtcrime.securesms.backup.ArchiveUploadProgress
|
import org.thoughtcrime.securesms.backup.ArchiveUploadProgress
|
||||||
import org.thoughtcrime.securesms.backup.LocalExportProgress
|
import org.thoughtcrime.securesms.backup.LocalExportProgress
|
||||||
@ -57,7 +57,6 @@ import org.thoughtcrime.securesms.jobs.LocalBackupJob
|
|||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
import org.thoughtcrime.securesms.keyvalue.protos.LocalBackupCreationProgress
|
import org.thoughtcrime.securesms.keyvalue.protos.LocalBackupCreationProgress
|
||||||
import org.thoughtcrime.securesms.net.SignalNetwork
|
import org.thoughtcrime.securesms.net.SignalNetwork
|
||||||
import org.thoughtcrime.securesms.providers.BlobProvider
|
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
@ -136,7 +135,7 @@ class InternalBackupPlaygroundViewModel : ViewModel() {
|
|||||||
|
|
||||||
fun validateBackup() {
|
fun validateBackup() {
|
||||||
_state.value = _state.value.copy(statusMessage = "Exporting to a temporary file...")
|
_state.value = _state.value.copy(statusMessage = "Exporting to a temporary file...")
|
||||||
val tempFile = BlobProvider.getInstance().forNonAutoEncryptingSingleSessionOnDisk(AppDependencies.application)
|
val tempFile = AppDependencies.blobs.forNonAutoEncryptingSingleSessionOnDisk(AppDependencies.application)
|
||||||
|
|
||||||
disposables += Single
|
disposables += Single
|
||||||
.fromCallable {
|
.fromCallable {
|
||||||
@ -207,7 +206,7 @@ class InternalBackupPlaygroundViewModel : ViewModel() {
|
|||||||
|
|
||||||
SignalExecutors.BOUNDED_IO.execute {
|
SignalExecutors.BOUNDED_IO.execute {
|
||||||
Log.d(TAG, "Downloading file...")
|
Log.d(TAG, "Downloading file...")
|
||||||
val tempBackupFile = BlobProvider.getInstance().forNonAutoEncryptingSingleSessionOnDisk(AppDependencies.application)
|
val tempBackupFile = AppDependencies.blobs.forNonAutoEncryptingSingleSessionOnDisk(AppDependencies.application)
|
||||||
|
|
||||||
when (val result = BackupRepository.downloadBackupFile(tempBackupFile)) {
|
when (val result = BackupRepository.downloadBackupFile(tempBackupFile)) {
|
||||||
is NetworkResult.Success -> Log.i(TAG, "Download successful")
|
is NetworkResult.Success -> Log.i(TAG, "Download successful")
|
||||||
|
|||||||
@ -9,7 +9,6 @@ sealed interface LabsSettingsEvents {
|
|||||||
data class ToggleIndividualChatPlaintextExport(val enabled: Boolean) : LabsSettingsEvents
|
data class ToggleIndividualChatPlaintextExport(val enabled: Boolean) : LabsSettingsEvents
|
||||||
data class ToggleStoryArchive(val enabled: Boolean) : LabsSettingsEvents
|
data class ToggleStoryArchive(val enabled: Boolean) : LabsSettingsEvents
|
||||||
data class ToggleIncognito(val enabled: Boolean) : LabsSettingsEvents
|
data class ToggleIncognito(val enabled: Boolean) : LabsSettingsEvents
|
||||||
data class ToggleGroupSuggestionsForMembers(val enabled: Boolean) : LabsSettingsEvents
|
|
||||||
data class ToggleBetterSearch(val enabled: Boolean) : LabsSettingsEvents
|
data class ToggleBetterSearch(val enabled: Boolean) : LabsSettingsEvents
|
||||||
data class ToggleAutoLowerHand(val enabled: Boolean) : LabsSettingsEvents
|
data class ToggleAutoLowerHand(val enabled: Boolean) : LabsSettingsEvents
|
||||||
data class ToggleStarredMessages(val enabled: Boolean) : LabsSettingsEvents
|
data class ToggleStarredMessages(val enabled: Boolean) : LabsSettingsEvents
|
||||||
|
|||||||
@ -116,15 +116,6 @@ private fun LabsSettingsContent(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
item {
|
|
||||||
Rows.ToggleRow(
|
|
||||||
checked = state.groupSuggestionsForMembers,
|
|
||||||
text = "Group Suggestions for Members",
|
|
||||||
label = "When creating a group, show existing groups that have the exact same members.",
|
|
||||||
onCheckChanged = { onEvent(LabsSettingsEvents.ToggleGroupSuggestionsForMembers(it)) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
item {
|
item {
|
||||||
Rows.ToggleRow(
|
Rows.ToggleRow(
|
||||||
checked = state.betterSearch,
|
checked = state.betterSearch,
|
||||||
|
|||||||
@ -12,7 +12,6 @@ data class LabsSettingsState(
|
|||||||
val individualChatPlaintextExport: Boolean = false,
|
val individualChatPlaintextExport: Boolean = false,
|
||||||
val storyArchive: Boolean = false,
|
val storyArchive: Boolean = false,
|
||||||
val incognito: Boolean = false,
|
val incognito: Boolean = false,
|
||||||
val groupSuggestionsForMembers: Boolean = false,
|
|
||||||
val betterSearch: Boolean = false,
|
val betterSearch: Boolean = false,
|
||||||
val autoLowerHand: Boolean = false,
|
val autoLowerHand: Boolean = false,
|
||||||
val starredMessages: Boolean = false
|
val starredMessages: Boolean = false
|
||||||
|
|||||||
@ -29,10 +29,6 @@ class LabsSettingsViewModel : ViewModel() {
|
|||||||
SignalStore.labs.incognito = event.enabled
|
SignalStore.labs.incognito = event.enabled
|
||||||
_state.value = _state.value.copy(incognito = event.enabled)
|
_state.value = _state.value.copy(incognito = event.enabled)
|
||||||
}
|
}
|
||||||
is LabsSettingsEvents.ToggleGroupSuggestionsForMembers -> {
|
|
||||||
SignalStore.labs.groupSuggestionsForMembers = event.enabled
|
|
||||||
_state.value = _state.value.copy(groupSuggestionsForMembers = event.enabled)
|
|
||||||
}
|
|
||||||
is LabsSettingsEvents.ToggleBetterSearch -> {
|
is LabsSettingsEvents.ToggleBetterSearch -> {
|
||||||
SignalStore.labs.betterSearch = event.enabled
|
SignalStore.labs.betterSearch = event.enabled
|
||||||
_state.value = _state.value.copy(betterSearch = event.enabled)
|
_state.value = _state.value.copy(betterSearch = event.enabled)
|
||||||
@ -54,7 +50,6 @@ class LabsSettingsViewModel : ViewModel() {
|
|||||||
individualChatPlaintextExport = SignalStore.labs.individualChatPlaintextExport,
|
individualChatPlaintextExport = SignalStore.labs.individualChatPlaintextExport,
|
||||||
storyArchive = SignalStore.labs.storyArchive,
|
storyArchive = SignalStore.labs.storyArchive,
|
||||||
incognito = SignalStore.labs.incognito,
|
incognito = SignalStore.labs.incognito,
|
||||||
groupSuggestionsForMembers = SignalStore.labs.groupSuggestionsForMembers,
|
|
||||||
betterSearch = SignalStore.labs.betterSearch,
|
betterSearch = SignalStore.labs.betterSearch,
|
||||||
autoLowerHand = SignalStore.labs.autoLowerHand,
|
autoLowerHand = SignalStore.labs.autoLowerHand,
|
||||||
starredMessages = SignalStore.labs.starredMessages
|
starredMessages = SignalStore.labs.starredMessages
|
||||||
|
|||||||
@ -274,13 +274,15 @@ private fun AdvancedPrivacySettingsScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
item {
|
if (state.isPrimaryDevice) {
|
||||||
Rows.ToggleRow(
|
item {
|
||||||
checked = state.allowSealedSenderFromAnyone,
|
Rows.ToggleRow(
|
||||||
text = stringResource(R.string.preferences_communication__sealed_sender_allow_from_anyone),
|
checked = state.allowSealedSenderFromAnyone,
|
||||||
label = stringResource(R.string.preferences_communication__sealed_sender_allow_from_anyone_description),
|
text = stringResource(R.string.preferences_communication__sealed_sender_allow_from_anyone),
|
||||||
onCheckChanged = callbacks::onAllowSealedSenderFromAnyoneChanged
|
label = stringResource(R.string.preferences_communication__sealed_sender_allow_from_anyone_description),
|
||||||
)
|
onCheckChanged = callbacks::onAllowSealedSenderFromAnyoneChanged
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
item {
|
item {
|
||||||
@ -342,7 +344,8 @@ private fun AdvancedPrivacySettingsScreenPreview() {
|
|||||||
showSealedSenderStatusIcon = false,
|
showSealedSenderStatusIcon = false,
|
||||||
allowSealedSenderFromAnyone = false,
|
allowSealedSenderFromAnyone = false,
|
||||||
showProgressSpinner = false,
|
showProgressSpinner = false,
|
||||||
allowAutomaticKeyVerification = false
|
allowAutomaticKeyVerification = false,
|
||||||
|
isPrimaryDevice = true
|
||||||
),
|
),
|
||||||
callbacks = AdvancedPrivacySettingsCallbacks.Empty
|
callbacks = AdvancedPrivacySettingsCallbacks.Empty
|
||||||
)
|
)
|
||||||
|
|||||||
@ -8,7 +8,8 @@ data class AdvancedPrivacySettingsState(
|
|||||||
val showSealedSenderStatusIcon: Boolean,
|
val showSealedSenderStatusIcon: Boolean,
|
||||||
val allowSealedSenderFromAnyone: Boolean,
|
val allowSealedSenderFromAnyone: Boolean,
|
||||||
val showProgressSpinner: Boolean,
|
val showProgressSpinner: Boolean,
|
||||||
val allowAutomaticKeyVerification: Boolean
|
val allowAutomaticKeyVerification: Boolean,
|
||||||
|
val isPrimaryDevice: Boolean
|
||||||
)
|
)
|
||||||
|
|
||||||
enum class CensorshipCircumventionState(val available: Boolean) {
|
enum class CensorshipCircumventionState(val available: Boolean) {
|
||||||
|
|||||||
@ -105,7 +105,8 @@ class AdvancedPrivacySettingsViewModel(
|
|||||||
AppDependencies.application
|
AppDependencies.application
|
||||||
),
|
),
|
||||||
showProgressSpinner = false,
|
showProgressSpinner = false,
|
||||||
allowAutomaticKeyVerification = SignalStore.settings.automaticVerificationEnabled
|
allowAutomaticKeyVerification = SignalStore.settings.automaticVerificationEnabled,
|
||||||
|
isPrimaryDevice = SignalStore.account.isPrimaryDevice
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -174,7 +174,7 @@ object RecurringInAppPaymentRepository {
|
|||||||
InAppPaymentsRepository.getSubscriber(subscriberType)?.subscriberId ?: SubscriberId.generate()
|
InAppPaymentsRepository.getSubscriber(subscriberType)?.subscriberId ?: SubscriberId.generate()
|
||||||
}
|
}
|
||||||
|
|
||||||
donationsService.putSubscription(subscriberId).resultOrThrow
|
donationsService.createSubscriber(subscriberId).resultOrThrow
|
||||||
|
|
||||||
Log.d(TAG, "Successfully set SubscriberId exists on Signal service.", true)
|
Log.d(TAG, "Successfully set SubscriberId exists on Signal service.", true)
|
||||||
|
|
||||||
|
|||||||
@ -42,6 +42,7 @@ import org.whispersystems.signalservice.api.subscriptions.SubscriberId
|
|||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
import java.text.DecimalFormat
|
import java.text.DecimalFormat
|
||||||
import java.text.DecimalFormatSymbols
|
import java.text.DecimalFormatSymbols
|
||||||
|
import java.text.ParseException
|
||||||
import java.util.Currency
|
import java.util.Currency
|
||||||
import java.util.Optional
|
import java.util.Optional
|
||||||
|
|
||||||
@ -170,6 +171,8 @@ class DonateToSignalViewModel(
|
|||||||
decimalFormat.parse(amount) as BigDecimal
|
decimalFormat.parse(amount) as BigDecimal
|
||||||
} catch (e: NumberFormatException) {
|
} catch (e: NumberFormatException) {
|
||||||
BigDecimal.ZERO
|
BigDecimal.ZERO
|
||||||
|
} catch (e: ParseException) {
|
||||||
|
BigDecimal.ZERO
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
package org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway
|
package org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway
|
||||||
|
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
import com.google.i18n.phonenumbers.PhoneNumberUtil
|
import com.google.i18n.phonenumbers.PhoneNumberUtil
|
||||||
import org.signal.core.util.orNull
|
import org.signal.core.util.orNull
|
||||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppDonations
|
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppDonations
|
||||||
@ -45,6 +46,10 @@ sealed interface GatewayOrderStrategy {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private data class Fixed(
|
||||||
|
override val orderedGateways: Set<InAppPaymentData.PaymentMethodType>
|
||||||
|
) : GatewayOrderStrategy
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun getStrategy(self: Recipient = Recipient.self()): GatewayOrderStrategy {
|
fun getStrategy(self: Recipient = Recipient.self()): GatewayOrderStrategy {
|
||||||
val e164 = self.e164.orNull() ?: return Default
|
val e164 = self.e164.orNull() ?: return Default
|
||||||
@ -57,5 +62,10 @@ sealed interface GatewayOrderStrategy {
|
|||||||
Default
|
Default
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
fun forTesting(vararg orderedGateways: InAppPaymentData.PaymentMethodType): GatewayOrderStrategy {
|
||||||
|
return Fixed(linkedSetOf(*orderedGateways))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -330,37 +330,52 @@ private fun GatewaySelectorBottomSheetContentReadyOneTimeGiftDonationPreview() {
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
fun rememberGatewaySelectorBottomSheetContentPreviewState(type: InAppPaymentType): GatewaySelectorState.Ready {
|
fun rememberGatewaySelectorBottomSheetContentPreviewState(
|
||||||
|
type: InAppPaymentType,
|
||||||
|
gatewayOrderStrategy: GatewayOrderStrategy = GatewayOrderStrategy.getStrategy(
|
||||||
|
self = Recipient(
|
||||||
|
isResolving = false,
|
||||||
|
e164Value = "+15555555555"
|
||||||
|
)
|
||||||
|
),
|
||||||
|
availableGateways: Set<InAppPaymentData.PaymentMethodType> = setOf(
|
||||||
|
InAppPaymentData.PaymentMethodType.GOOGLE_PAY,
|
||||||
|
InAppPaymentData.PaymentMethodType.PAYPAL,
|
||||||
|
InAppPaymentData.PaymentMethodType.CARD,
|
||||||
|
InAppPaymentData.PaymentMethodType.SEPA_DEBIT,
|
||||||
|
InAppPaymentData.PaymentMethodType.IDEAL
|
||||||
|
)
|
||||||
|
): GatewaySelectorState.Ready {
|
||||||
return remember {
|
return remember {
|
||||||
GatewaySelectorState.Ready(
|
GatewaySelectorState.Ready(
|
||||||
inAppPayment = InAppPaymentTable.InAppPayment(
|
inAppPayment = createInAppPaymentPreview(type),
|
||||||
id = InAppPaymentTable.InAppPaymentId(1),
|
gatewayOrderStrategy = gatewayOrderStrategy,
|
||||||
type = type,
|
isGooglePayAvailable = InAppPaymentData.PaymentMethodType.GOOGLE_PAY in availableGateways,
|
||||||
state = InAppPaymentTable.State.CREATED,
|
isPayPalAvailable = InAppPaymentData.PaymentMethodType.PAYPAL in availableGateways,
|
||||||
insertedAt = 1.milliseconds,
|
isCreditCardAvailable = InAppPaymentData.PaymentMethodType.CARD in availableGateways,
|
||||||
updatedAt = 1.milliseconds,
|
isSEPADebitAvailable = InAppPaymentData.PaymentMethodType.SEPA_DEBIT in availableGateways,
|
||||||
notified = true,
|
isIDEALAvailable = InAppPaymentData.PaymentMethodType.IDEAL in availableGateways,
|
||||||
subscriberId = null,
|
|
||||||
endOfPeriod = 0.milliseconds,
|
|
||||||
data = InAppPaymentData(
|
|
||||||
badge = BadgeList.Badge(
|
|
||||||
name = type.name.lowercase()
|
|
||||||
),
|
|
||||||
amount = FiatValue(currencyCode = "USD", amount = BigDecimal.TEN.toDecimalValue())
|
|
||||||
)
|
|
||||||
),
|
|
||||||
gatewayOrderStrategy = GatewayOrderStrategy.getStrategy(
|
|
||||||
self = Recipient(
|
|
||||||
isResolving = false,
|
|
||||||
e164Value = "+15555555555"
|
|
||||||
)
|
|
||||||
),
|
|
||||||
isGooglePayAvailable = true,
|
|
||||||
isPayPalAvailable = true,
|
|
||||||
isCreditCardAvailable = true,
|
|
||||||
isSEPADebitAvailable = true,
|
|
||||||
isIDEALAvailable = true,
|
|
||||||
sepaEuroMaximum = FiatMoney(BigDecimal.ONE, Currency.getInstance("USD"))
|
sepaEuroMaximum = FiatMoney(BigDecimal.ONE, Currency.getInstance("USD"))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
fun createInAppPaymentPreview(type: InAppPaymentType): InAppPaymentTable.InAppPayment {
|
||||||
|
return InAppPaymentTable.InAppPayment(
|
||||||
|
id = InAppPaymentTable.InAppPaymentId(1),
|
||||||
|
type = type,
|
||||||
|
state = InAppPaymentTable.State.CREATED,
|
||||||
|
insertedAt = 1.milliseconds,
|
||||||
|
updatedAt = 1.milliseconds,
|
||||||
|
notified = true,
|
||||||
|
subscriberId = null,
|
||||||
|
endOfPeriod = 0.milliseconds,
|
||||||
|
data = InAppPaymentData(
|
||||||
|
badge = BadgeList.Badge(
|
||||||
|
name = type.name.lowercase()
|
||||||
|
),
|
||||||
|
amount = FiatValue(currencyCode = "USD", amount = BigDecimal.TEN.toDecimalValue())
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@ -185,7 +185,7 @@ class BankTransferDetailsFragment : ComposeFragment(), InAppPaymentCheckoutDeleg
|
|||||||
|
|
||||||
@DayNightPreviews
|
@DayNightPreviews
|
||||||
@Composable
|
@Composable
|
||||||
private fun BankTransferDetailsContentPreview() {
|
fun BankTransferDetailsContentPreview() {
|
||||||
Previews.Preview {
|
Previews.Preview {
|
||||||
BankTransferDetailsContent(
|
BankTransferDetailsContent(
|
||||||
state = BankTransferDetailsState(
|
state = BankTransferDetailsState(
|
||||||
@ -206,7 +206,7 @@ private fun BankTransferDetailsContentPreview() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun BankTransferDetailsContent(
|
fun BankTransferDetailsContent(
|
||||||
state: BankTransferDetailsState,
|
state: BankTransferDetailsState,
|
||||||
onNavigationClick: () -> Unit,
|
onNavigationClick: () -> Unit,
|
||||||
onNameChanged: (String) -> Unit,
|
onNameChanged: (String) -> Unit,
|
||||||
|
|||||||
@ -44,16 +44,19 @@ import androidx.navigation.navGraphViewModels
|
|||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import org.signal.core.ui.compose.Buttons
|
import org.signal.core.ui.compose.Buttons
|
||||||
import org.signal.core.ui.compose.ComposeFragment
|
import org.signal.core.ui.compose.ComposeFragment
|
||||||
|
import org.signal.core.ui.compose.Previews
|
||||||
import org.signal.core.ui.compose.Scaffolds
|
import org.signal.core.ui.compose.Scaffolds
|
||||||
import org.signal.core.ui.compose.SignalIcons
|
import org.signal.core.ui.compose.SignalIcons
|
||||||
import org.signal.core.ui.compose.Texts
|
import org.signal.core.ui.compose.Texts
|
||||||
import org.signal.core.util.getParcelableCompat
|
import org.signal.core.util.getParcelableCompat
|
||||||
|
import org.signal.donations.InAppPaymentType
|
||||||
import org.thoughtcrime.securesms.R
|
import org.thoughtcrime.securesms.R
|
||||||
import org.thoughtcrime.securesms.components.TemporaryScreenshotSecurity
|
import org.thoughtcrime.securesms.components.TemporaryScreenshotSecurity
|
||||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationSerializationHelper.toFiatMoney
|
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationSerializationHelper.toFiatMoney
|
||||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.InAppPaymentCheckoutDelegate
|
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.InAppPaymentCheckoutDelegate
|
||||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.InAppPaymentProcessorAction
|
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.InAppPaymentProcessorAction
|
||||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.InAppPaymentProcessorActionResult
|
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.InAppPaymentProcessorActionResult
|
||||||
|
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.createInAppPaymentPreview
|
||||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.stripe.StripePaymentInProgressFragment
|
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.stripe.StripePaymentInProgressFragment
|
||||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.stripe.StripePaymentInProgressViewModel
|
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.stripe.StripePaymentInProgressViewModel
|
||||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.transfer.BankTransferRequestKeys
|
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.transfer.BankTransferRequestKeys
|
||||||
@ -189,19 +192,25 @@ class IdealTransferDetailsFragment : ComposeFragment(), InAppPaymentCheckoutDele
|
|||||||
|
|
||||||
@Preview
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
private fun IdealTransferDetailsContentPreview() {
|
fun IdealTransferDetailsContentPreview() {
|
||||||
IdealTransferDetailsContent(
|
Previews.Preview {
|
||||||
state = IdealTransferDetailsState(),
|
IdealTransferDetailsContent(
|
||||||
idealDirections = R.string.IdealTransferDetailsFragment__enter_your_bank,
|
state = IdealTransferDetailsState(
|
||||||
donateLabel = "Donate $5/month",
|
inAppPayment = createInAppPaymentPreview(InAppPaymentType.RECURRING_DONATION),
|
||||||
onNavigationClick = {},
|
name = "Miles Morales",
|
||||||
onLearnMoreClick = {},
|
email = "miles@example.com"
|
||||||
onSelectBankClick = {},
|
),
|
||||||
onNameChanged = {},
|
idealDirections = R.string.IdealTransferDetailsFragment__enter_your_bank,
|
||||||
onEmailChanged = {},
|
donateLabel = "Donate $5/month",
|
||||||
onFocusChanged = { _, _ -> },
|
onNavigationClick = {},
|
||||||
onDonateClick = {}
|
onLearnMoreClick = {},
|
||||||
)
|
onSelectBankClick = {},
|
||||||
|
onNameChanged = {},
|
||||||
|
onEmailChanged = {},
|
||||||
|
onFocusChanged = { _, _ -> },
|
||||||
|
onDonateClick = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
@ -0,0 +1,48 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2026 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.thoughtcrime.securesms.components.settings.app.subscription.permits
|
||||||
|
|
||||||
|
import androidx.annotation.WorkerThread
|
||||||
|
import org.signal.core.util.Base64
|
||||||
|
import org.signal.donations.permits.DonationPermitError
|
||||||
|
import org.signal.libsignal.net.RequestResult
|
||||||
|
import org.signal.network.rest.RestStatusCodeError
|
||||||
|
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||||
|
import org.whispersystems.signalservice.api.donations.DonationPermitProvider
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
/**
|
||||||
|
* App-side [DonationPermitProvider]: spends a permit and base64-encodes it for the `Donation-Permit` header,
|
||||||
|
* translating an acquisition failure into a [RequestResult] the donations service can surface.
|
||||||
|
*/
|
||||||
|
object DonationPermits : DonationPermitProvider {
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
override fun getDonationPermit(): RequestResult<String, RestStatusCodeError> {
|
||||||
|
return AppDependencies.donationPermitsRepository
|
||||||
|
.spendOrAcquirePermit()
|
||||||
|
.fold(
|
||||||
|
ifLeft = { it.toRequestResult() },
|
||||||
|
ifRight = { RequestResult.Success(Base64.encodeWithPadding(it.serialize())) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun DonationPermitError.toRequestResult(): RequestResult<String, RestStatusCodeError> {
|
||||||
|
return when (this) {
|
||||||
|
is DonationPermitError.IssuerUnavailable -> {
|
||||||
|
val statusCode = statusCode
|
||||||
|
val cause = cause
|
||||||
|
when {
|
||||||
|
statusCode != null -> RequestResult.NonSuccess(RestStatusCodeError(statusCode, emptyMap(), null))
|
||||||
|
cause is IOException -> RequestResult.RetryableNetworkError(cause)
|
||||||
|
else -> RequestResult.ApplicationError(cause ?: IllegalStateException("Donation permit issuer unavailable"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DonationPermitError.VerificationFailed -> RequestResult.ApplicationError(IllegalStateException("Donation permit verification failed"))
|
||||||
|
DonationPermitError.MalformedResponse -> RequestResult.ApplicationError(IllegalStateException("Malformed donation permit response"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2026 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.thoughtcrime.securesms.components.settings.app.subscription.permits
|
||||||
|
|
||||||
|
import arrow.core.Either
|
||||||
|
import arrow.core.left
|
||||||
|
import arrow.core.right
|
||||||
|
import org.signal.donations.permits.DonationPermitError
|
||||||
|
import org.signal.donations.permits.DonationPermitIssuer
|
||||||
|
import org.signal.libsignal.net.RequestResult
|
||||||
|
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [DonationPermitIssuer] backed by [org.whispersystems.signalservice.api.donations.DonationsApi], translating a
|
||||||
|
* non-success [RequestResult] into a [DonationPermitError].
|
||||||
|
*/
|
||||||
|
object NetworkDonationPermitIssuer : DonationPermitIssuer {
|
||||||
|
|
||||||
|
override fun issue(requestBytes: ByteArray): Either<DonationPermitError, ByteArray> {
|
||||||
|
return when (val result = AppDependencies.donationsApi.createDonationPermits(requestBytes)) {
|
||||||
|
is RequestResult.Success -> result.result.right()
|
||||||
|
is RequestResult.NonSuccess -> DonationPermitError.IssuerUnavailable(statusCode = result.error.statusCode).left()
|
||||||
|
is RequestResult.RetryableNetworkError -> DonationPermitError.IssuerUnavailable(cause = result.networkError).left()
|
||||||
|
is RequestResult.ApplicationError -> DonationPermitError.IssuerUnavailable(cause = result.cause).left()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -24,8 +24,8 @@ import kotlinx.coroutines.withContext
|
|||||||
import org.signal.core.util.logging.Log
|
import org.signal.core.util.logging.Log
|
||||||
import org.thoughtcrime.securesms.R
|
import org.thoughtcrime.securesms.R
|
||||||
import org.thoughtcrime.securesms.database.model.InAppPaymentReceiptRecord
|
import org.thoughtcrime.securesms.database.model.InAppPaymentReceiptRecord
|
||||||
|
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||||
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
|
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
|
||||||
import org.thoughtcrime.securesms.providers.BlobProvider
|
|
||||||
import org.thoughtcrime.securesms.util.DateUtils
|
import org.thoughtcrime.securesms.util.DateUtils
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
@ -75,7 +75,7 @@ object ReceiptImageRenderer {
|
|||||||
val bitmap = view.drawToBitmap()
|
val bitmap = view.drawToBitmap()
|
||||||
bitmap.compress(Bitmap.CompressFormat.PNG, 0, outputStream)
|
bitmap.compress(Bitmap.CompressFormat.PNG, 0, outputStream)
|
||||||
|
|
||||||
BlobProvider.getInstance()
|
AppDependencies.blobs
|
||||||
.forData(outputStream.toByteArray())
|
.forData(outputStream.toByteArray())
|
||||||
.withMimeType("image/png")
|
.withMimeType("image/png")
|
||||||
.withFileName("Signal-Donation-Receipt.png")
|
.withFileName("Signal-Donation-Receipt.png")
|
||||||
|
|||||||
@ -75,10 +75,11 @@ import org.thoughtcrime.securesms.components.settings.app.usernamelinks.QrCodeDa
|
|||||||
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.QrCodeState
|
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.QrCodeState
|
||||||
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.UsernameQrCodeColorScheme
|
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.UsernameQrCodeColorScheme
|
||||||
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.main.UsernameLinkSettingsState.ActiveTab
|
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.main.UsernameLinkSettingsState.ActiveTab
|
||||||
import org.thoughtcrime.securesms.providers.BlobProvider
|
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||||
import org.thoughtcrime.securesms.util.CommunicationActions
|
import org.thoughtcrime.securesms.util.CommunicationActions
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
import org.signal.mediasend.R as MediaSendR
|
||||||
|
|
||||||
@OptIn(ExperimentalPermissionsApi::class)
|
@OptIn(ExperimentalPermissionsApi::class)
|
||||||
class UsernameLinkSettingsFragment : ComposeFragment() {
|
class UsernameLinkSettingsFragment : ComposeFragment() {
|
||||||
@ -158,8 +159,8 @@ class UsernameLinkSettingsFragment : ComposeFragment() {
|
|||||||
Permissions.with(this)
|
Permissions.with(this)
|
||||||
.request(Manifest.permission.CAMERA)
|
.request(Manifest.permission.CAMERA)
|
||||||
.ifNecessary()
|
.ifNecessary()
|
||||||
.withPermanentDenialDialog(getString(R.string.CameraXFragment_signal_needs_camera_access_scan_qr_code), null, R.string.CameraXFragment_allow_access_camera, R.string.CameraXFragment_to_scan_qr_codes, parentFragmentManager)
|
.withPermanentDenialDialog(getString(MediaSendR.string.CameraXFragment_signal_needs_camera_access_scan_qr_code), null, MediaSendR.string.CameraXFragment_allow_access_camera, MediaSendR.string.CameraXFragment_to_scan_qr_codes, parentFragmentManager)
|
||||||
.onAnyDenied { Toast.makeText(requireContext(), R.string.CameraXFragment_signal_needs_camera_access_scan_qr_code, Toast.LENGTH_LONG).show() }
|
.onAnyDenied { Toast.makeText(requireContext(), MediaSendR.string.CameraXFragment_signal_needs_camera_access_scan_qr_code, Toast.LENGTH_LONG).show() }
|
||||||
.execute()
|
.execute()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -404,7 +405,7 @@ private fun shareQrBadge(activity: Activity, badge: Bitmap?) {
|
|||||||
badge.compress(Bitmap.CompressFormat.PNG, 100, byteArrayOutputStream)
|
badge.compress(Bitmap.CompressFormat.PNG, 100, byteArrayOutputStream)
|
||||||
byteArrayOutputStream.flush()
|
byteArrayOutputStream.flush()
|
||||||
val bytes = byteArrayOutputStream.toByteArray()
|
val bytes = byteArrayOutputStream.toByteArray()
|
||||||
val shareUri = BlobProvider.getInstance()
|
val shareUri = AppDependencies.blobs
|
||||||
.forData(bytes)
|
.forData(bytes)
|
||||||
.withMimeType("image/png")
|
.withMimeType("image/png")
|
||||||
.withFileName("SignalUsernameQr.png")
|
.withFileName("SignalUsernameQr.png")
|
||||||
|
|||||||
@ -36,6 +36,7 @@ import org.signal.core.ui.compose.theme.SignalTheme
|
|||||||
import org.thoughtcrime.securesms.R
|
import org.thoughtcrime.securesms.R
|
||||||
import org.thoughtcrime.securesms.qr.QrCrosshair
|
import org.thoughtcrime.securesms.qr.QrCrosshair
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
|
import org.signal.mediasend.R as MediaSendR
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A screen that allows you to scan a QR code to start a chat.
|
* A screen that allows you to scan a QR code to start a chat.
|
||||||
@ -116,7 +117,7 @@ fun UsernameQrScanScreen(
|
|||||||
.padding(48.dp)
|
.padding(48.dp)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.CameraXFragment_to_scan_qr_code_allow_camera),
|
text = stringResource(MediaSendR.string.CameraXFragment_to_scan_qr_code_allow_camera),
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
color = Color.White
|
color = Color.White
|
||||||
@ -125,7 +126,7 @@ fun UsernameQrScanScreen(
|
|||||||
colors = ButtonDefaults.filledTonalButtonColors(),
|
colors = ButtonDefaults.filledTonalButtonColors(),
|
||||||
onClick = onOpenCameraClicked
|
onClick = onOpenCameraClicked
|
||||||
) {
|
) {
|
||||||
Text(stringResource(R.string.CameraXFragment_allow_access))
|
Text(stringResource(MediaSendR.string.CameraXFragment_allow_access))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -47,6 +47,7 @@ import org.signal.core.util.permissions.PermissionCompat
|
|||||||
import org.thoughtcrime.securesms.R
|
import org.thoughtcrime.securesms.R
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||||
|
import org.signal.mediasend.R as MediaSendR
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prompts the user to scan a username QR code. Uses the activity result to communicate the recipient that was found, or null if no valid usernames were scanned.
|
* Prompts the user to scan a username QR code. Uses the activity result to communicate the recipient that was found, or null if no valid usernames were scanned.
|
||||||
@ -132,8 +133,8 @@ class UsernameQrScannerActivity : AppCompatActivity() {
|
|||||||
Permissions.with(this)
|
Permissions.with(this)
|
||||||
.request(Manifest.permission.CAMERA)
|
.request(Manifest.permission.CAMERA)
|
||||||
.ifNecessary()
|
.ifNecessary()
|
||||||
.withPermanentDenialDialog(getString(R.string.CameraXFragment_signal_needs_camera_access_scan_qr_code), null, R.string.CameraXFragment_allow_access_camera, R.string.CameraXFragment_to_scan_qr_codes, supportFragmentManager)
|
.withPermanentDenialDialog(getString(MediaSendR.string.CameraXFragment_signal_needs_camera_access_scan_qr_code), null, MediaSendR.string.CameraXFragment_allow_access_camera, MediaSendR.string.CameraXFragment_to_scan_qr_codes, supportFragmentManager)
|
||||||
.onAnyDenied { Toast.makeText(this, R.string.CameraXFragment_signal_needs_camera_access_scan_qr_code, Toast.LENGTH_LONG).show() }
|
.onAnyDenied { Toast.makeText(this, MediaSendR.string.CameraXFragment_signal_needs_camera_access_scan_qr_code, Toast.LENGTH_LONG).show() }
|
||||||
.execute()
|
.execute()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
package org.thoughtcrime.securesms.components.settings.conversation
|
package org.thoughtcrime.securesms.components.settings.conversation
|
||||||
|
|
||||||
import android.Manifest
|
|
||||||
import android.app.ActivityOptions
|
import android.app.ActivityOptions
|
||||||
import android.content.ActivityNotFoundException
|
import android.content.ActivityNotFoundException
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
@ -96,7 +95,6 @@ import org.thoughtcrime.securesms.main.MainNavigationChatDetailRouter
|
|||||||
import org.thoughtcrime.securesms.main.MainNavigationDetailLocation
|
import org.thoughtcrime.securesms.main.MainNavigationDetailLocation
|
||||||
import org.thoughtcrime.securesms.mediaoverview.MediaOverviewActivity
|
import org.thoughtcrime.securesms.mediaoverview.MediaOverviewActivity
|
||||||
import org.thoughtcrime.securesms.mediapreview.MediaIntentFactory
|
import org.thoughtcrime.securesms.mediapreview.MediaIntentFactory
|
||||||
import org.thoughtcrime.securesms.mediasend.camerax.CameraXRemoteConfig
|
|
||||||
import org.thoughtcrime.securesms.messagerequests.MessageRequestRepository
|
import org.thoughtcrime.securesms.messagerequests.MessageRequestRepository
|
||||||
import org.thoughtcrime.securesms.nicknames.NicknameActivity
|
import org.thoughtcrime.securesms.nicknames.NicknameActivity
|
||||||
import org.thoughtcrime.securesms.profiles.edit.CreateProfileActivity
|
import org.thoughtcrime.securesms.profiles.edit.CreateProfileActivity
|
||||||
@ -481,23 +479,8 @@ class ConversationSettingsFragment :
|
|||||||
.setMessage(R.string.ConversationSettingsFragment__only_admins_of_this_group_can_add_to_its_story)
|
.setMessage(R.string.ConversationSettingsFragment__only_admins_of_this_group_can_add_to_its_story)
|
||||||
.setPositiveButton(android.R.string.ok) { d, _ -> d.dismiss() }
|
.setPositiveButton(android.R.string.ok) { d, _ -> d.dismiss() }
|
||||||
.show()
|
.show()
|
||||||
} else if (CameraXRemoteConfig.isSupported()) {
|
|
||||||
addToGroupStoryDelegate.addToStory(state.recipient.id)
|
|
||||||
} else {
|
} else {
|
||||||
Permissions.with(this@ConversationSettingsFragment)
|
addToGroupStoryDelegate.addToStory(state.recipient.id)
|
||||||
.request(Manifest.permission.CAMERA)
|
|
||||||
.ifNecessary()
|
|
||||||
.withRationaleDialog(getString(R.string.CameraXFragment_allow_access_camera), getString(R.string.CameraXFragment_to_capture_photos_and_video_allow_camera), CoreUiR.drawable.symbol_camera_24)
|
|
||||||
.withPermanentDenialDialog(
|
|
||||||
getString(R.string.CameraXFragment_signal_needs_camera_access_capture_photos),
|
|
||||||
null,
|
|
||||||
R.string.CameraXFragment_allow_access_camera,
|
|
||||||
R.string.CameraXFragment_to_capture_photos_videos,
|
|
||||||
getParentFragmentManager()
|
|
||||||
)
|
|
||||||
.onAllGranted { addToGroupStoryDelegate.addToStory(state.recipient.id) }
|
|
||||||
.onAnyDenied { Toast.makeText(requireContext(), R.string.CameraXFragment_signal_needs_camera_access_capture_photos, Toast.LENGTH_LONG).show() }
|
|
||||||
.execute()
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onVideoClick = {
|
onVideoClick = {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user