Compare commits

...

106 Commits

Author SHA1 Message Date
Michelle Tang 205549360a Bump version to 8.21.0 2026-07-27 16:04:34 -04:00
Michelle Tang 09f8f456f6 Update baseline profile. 2026-07-27 15:18:45 -04:00
Michelle Tang 4d94ad8929 Update translations and other static files. 2026-07-27 15:05:53 -04:00
Michelle Tang e3e86d1b65 Check poll threads. 2026-07-27 14:56:51 -04:00
Cody Henthorne b535d3f473 Fix missing group members and p2p update send after joining group via link. 2026-07-27 14:56:51 -04:00
Cody Henthorne 0b7eda3e94 Force receipient refresh on any message request state change. 2026-07-27 14:56:51 -04:00
Ehren Kret 2162fccd7d Fix base64 decode issue with short signal.me URLs. 2026-07-27 14:56:51 -04:00
Ehren Kret 9ca47f5fc5 Fix single character group links crashing. 2026-07-27 14:56:51 -04:00
Greyson Parrelli e458b3ff66 Fix bug where you're repeatedly asked to enter AEP. 2026-07-27 14:56:51 -04:00
Greyson Parrelli 7a34150319 Add stickers to storage service. 2026-07-27 14:56:50 -04:00
Greyson Parrelli 71bd8f9ec3 Improve error message when you can't get a voice call for verification code. 2026-07-27 14:56:50 -04:00
Michelle Tang ad3d537d92 Add more KT fixes. 2026-07-27 14:56:50 -04:00
Cody Henthorne b019aba3fc Ensure group updates render before message content when not from p2p. 2026-07-27 14:56:50 -04:00
Greyson Parrelli bd5c60d442 More edge-to-edge support. 2026-07-27 14:56:50 -04:00
Michelle Tang a57572cf64 Change character limit for poll questions. 2026-07-27 14:56:49 -04:00
Greyson Parrelli 59bd04d9b7 Temporarily disable dependabot PRs. 2026-07-27 14:56:49 -04:00
Alex Hart 7f4829c80c Fix bad last-end-of-period check causing excess backup keep-alive requests. 2026-07-27 14:56:49 -04:00
Greyson Parrelli 70be67b267 Require users to correct intense clock skew. 2026-07-27 14:56:49 -04:00
Greyson Parrelli 6dc0229e72 First pass at edge-to-edge support. 2026-07-27 14:56:48 -04:00
Greyson Parrelli 338283eff5 Include backup tier in support email. 2026-07-27 14:56:48 -04:00
Greyson Parrelli 9804c6cf5e Improve call message validations. 2026-07-27 14:56:48 -04:00
Greyson Parrelli 072185ec36 Improve sync message validations. 2026-07-27 14:56:48 -04:00
Cody Henthorne d239f503a8 Bump libsignal to v0.99.1 2026-07-27 14:56:48 -04:00
Michelle Tang 12785f35c9 Improve group deletion.
Co-authored-by: Cody Henthorne <cody@signal.org>
2026-07-27 14:56:47 -04:00
Greyson Parrelli c8dfc74d41 Improve pin message validations. 2026-07-27 14:56:47 -04:00
Greyson Parrelli 5b42b115db Fix search crash when case-folding changes snippet length. 2026-07-27 14:56:47 -04:00
Cody Henthorne 22f8996d3f Additional UI tweaks to link account screen. 2026-07-27 14:56:47 -04:00
Greyson Parrelli a5fe925958 Reject zero-frame APNGs. 2026-07-27 14:56:46 -04:00
Greyson Parrelli ba4d92de45 Improve story context validation. 2026-07-27 14:56:46 -04:00
Greyson Parrelli 3e8190637c Improve attachment field validation. 2026-07-27 14:56:46 -04:00
Greyson Parrelli 7ea3679a24 Attempt to free up more disk space on GitHub CI. 2026-07-27 14:56:46 -04:00
Cody Henthorne 5a6c41e166 Reduce frequency of restore progress state logs. 2026-07-27 14:56:46 -04:00
Greyson Parrelli 263d7b7df1 Bump libsignal to v0.99.0 2026-07-27 14:56:46 -04:00
Greyson Parrelli be9ae5c8f2 Remove some unnecessary transcode size limits. 2026-07-27 14:56:45 -04:00
Greyson Parrelli 635b5795ea Notify user when a video is trimmed. 2026-07-27 14:56:45 -04:00
Greyson Parrelli cfce0ad8f1 Fix video transcode settings interpretation. 2026-07-27 14:56:45 -04:00
Cody Henthorne d55d1a2acd Fix UI issues in reg flow and update screenshot tests for all tablets. 2026-07-27 14:56:45 -04:00
Greyson Parrelli 0451ea188d Fix reg string to remove concatenation. 2026-07-27 14:56:45 -04:00
Greyson Parrelli 01a2599c3c Fix issue with null nextSms/Call times. 2026-07-27 14:56:45 -04:00
andrew-signal 682e5e4a01 Bump libsignal to v0.98.0 2026-07-27 14:56:44 -04:00
Greyson Parrelli 11ea9cb7b4 Improve backup notification title. 2026-07-27 14:56:44 -04:00
Greyson Parrelli 687818fbb5 Improve local archive folder structure recognition. 2026-07-27 14:56:44 -04:00
Greyson Parrelli 7a2c084b49 Remove unnecessary leading digits in phone number. 2026-07-27 14:56:44 -04:00
Greyson Parrelli dfe6ae812d Add locking around storage controller interactions. 2026-07-27 14:56:44 -04:00
Greyson Parrelli 8b1c2bf2af Fix RegistrationEndToEndTest flakiness. 2026-07-27 14:56:44 -04:00
Cody Henthorne ab61b79028 Move SignalSymbols to core-ui. 2026-07-27 14:56:44 -04:00
Greyson Parrelli 2d04bb32fd Add notifications for scheduled notes to self. 2026-07-27 14:56:43 -04:00
Michelle Tang 836645ed2b Add more logging around clearing KT data. 2026-07-27 14:56:43 -04:00
Greyson Parrelli dd32a92104 Add support for small link preview thumbnails. 2026-07-27 14:56:43 -04:00
Greyson Parrelli 4817b5a7ac Use secure-delete flag for smaller deletes. 2026-07-27 14:56:43 -04:00
adel-signal e0603b14bb Update RingRTC to v2.70.0, add video config settings
Co-authored-by: Cody Henthorne <cody@signal.org>
2026-07-27 14:56:43 -04:00
Greyson Parrelli 5f22566e10 Create and use RegistrationApiV2. 2026-07-27 14:56:43 -04:00
Greyson Parrelli 6f5d7c4b7b Normalize sticker tables. 2026-07-27 14:56:42 -04:00
Cody Henthorne 393f58c833 Fix signal data usage under reporting. 2026-07-27 14:56:42 -04:00
Cody Henthorne d25ac71907 Ignore renderable content in group update messages. 2026-07-27 14:56:42 -04:00
Cody Henthorne 0df6c66d08 Catch invalid group master key creation. 2026-07-27 14:56:42 -04:00
Greyson Parrelli 9abe34bd69 Remove camera requirement. 2026-07-27 14:56:42 -04:00
Cody Henthorne 8e54254ffa Recover attachments stuck mid-restore so media restore can complete. 2026-07-27 14:56:42 -04:00
Greyson Parrelli 0b6cd03115 Improve UX around max linked devices. 2026-07-27 14:56:42 -04:00
Greyson Parrelli 07500f62ab Upgrade SQLCipher to 4.17.0 2026-07-20 16:56:30 -04:00
Cody Henthorne 3cbc61cd11 Hide remote mute option in participant context menu for 1:1 calls. 2026-07-20 16:40:26 -04:00
Cody Henthorne 1be2aeaf2b Retry/resume link+sync backup upload improvements. 2026-07-20 16:34:45 -04:00
dependabot[bot] b9012e2820 ci: bump the actions group with 2 updates
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-20 16:18:18 -04:00
Greyson Parrelli e9249f33a5 Fix screenshot test for translation. 2026-07-20 15:56:56 -04:00
Cody Henthorne 7af82fbc87 Prevent call PiP from appearing in outgoing screen share. 2026-07-20 13:40:54 -04:00
Cody Henthorne 8b71bde890 Fix checkout tests hitting chat connection calls. 2026-07-20 13:40:41 -04:00
Greyson Parrelli 1ae5e884ec Remove duplicate preExistingRegistrationData test helper introduced during rebase. 2026-07-20 13:17:03 -04:00
Greyson Parrelli a30cacdb0c Add registration test for bad RRP during reglock. 2026-07-20 13:12:51 -04:00
Greyson Parrelli 0a18d01801 Do not allow re-registers to restore data in regV5. 2026-07-20 13:12:51 -04:00
Greyson Parrelli 4aa5447967 Upgrade to glide 5.0.9 2026-07-20 13:12:05 -04:00
Greyson Parrelli a92079e31b Add labs feature to have delete placeholders. 2026-07-20 13:12:05 -04:00
Alex Hart 628576817e Update donation receipt string. 2026-07-20 13:12:05 -04:00
Greyson Parrelli 297b7fedd3 Update compileSdk to 37. 2026-07-20 13:12:05 -04:00
andrew-signal c0283fb75f Migrate to libsignal getDevices/removeDevice.
Co-authored-by: Cody Henthorne <cody@signal.org>
2026-07-20 13:12:04 -04:00
Greyson Parrelli 70e768ba05 Allow registration demo to run on minSdk 23. 2026-07-20 13:12:04 -04:00
Greyson Parrelli 551a25cfc6 Clean up orphaned latest_revision_id references during backup restore. 2026-07-20 13:12:04 -04:00
Cody Henthorne 39da95f29b Auto-resend media/story send after trusting safety number change. 2026-07-20 13:12:04 -04:00
andrew-signal 35ca16f294 Migrate to libsignal deleteUsernameHash.
Co-authored-by: Cody Henthorne <cody@signal.org>
2026-07-20 13:12:04 -04:00
andrew-signal 7e79dfa57f Migrate to libsignal setPushToken/clearPushToken.
Co-authored-by: Cody Henthorne <cody@signal.org>
# Conflicts:
#	lib/libsignal-service/src/main/java/org/whispersystems/signalservice/api/account/AccountApi.kt
2026-07-20 13:12:04 -04:00
andrew-signal abf05293eb Bump libsignal to v0.97.4 2026-07-20 13:12:04 -04:00
Greyson Parrelli e81098378f Attempt to free disk space in CI for reproducible build check.
We were running out of disk space during the operation.
This attempts to remove some usused stuff to allow us to hopefully
complete the operation successfully.
2026-07-20 13:12:04 -04:00
Greyson Parrelli 28d2dee617 Potentially resolve bad service starts on BOOT_COMPLETED. 2026-07-20 13:12:04 -04:00
Greyson Parrelli 7ad05dd53c Move some network classes to :core:network. 2026-07-20 13:12:04 -04:00
Greyson Parrelli 75036acce5 Warn users if they enter the verification code as their PIN. 2026-07-20 13:12:03 -04:00
Alex Hart 1bebab13db Change camera control color. 2026-07-20 13:10:48 -04:00
Greyson Parrelli ec2a8d37d6 Improve edit message validations. 2026-07-20 13:10:48 -04:00
Greyson Parrelli ce05c60eb4 Improve rich content URI validation. 2026-07-20 13:10:48 -04:00
Greyson Parrelli d818a93928 Apply announcement-only admin check to incoming group stories. 2026-07-20 13:10:48 -04:00
Greyson Parrelli 4af791c903 Improve emoji regex perf. 2026-07-20 13:10:48 -04:00
Alex Hart dcef0c9667 Stop call link clustering. 2026-07-20 13:10:48 -04:00
Greyson Parrelli 974e2efad5 Improve story body range validations. 2026-07-20 13:10:48 -04:00
Greyson Parrelli 85261afe42 Preserve semicolons within linkified URLs. 2026-07-20 13:10:48 -04:00
Alex Hart 1f7c42c24b Refresh Camera UI for large screens. 2026-07-20 13:10:48 -04:00
Greyson Parrelli 4888805873 Bump version to 8.20.4 2026-07-20 13:09:58 -04:00
Greyson Parrelli c27dc2a42d Update baseline profile. 2026-07-20 13:09:37 -04:00
Greyson Parrelli 17b806ae0e Update translations and other static files. 2026-07-20 12:58:19 -04:00
Cody Henthorne 3fbce0c774 Do not respect optimized media setting from primary on link+sync. 2026-07-20 11:32:50 -04:00
Cody Henthorne 7c464a7759 Never sync trim deletes from linked devices. 2026-07-20 11:21:25 -04:00
Greyson Parrelli 0e1d60bf1d Do not allow re-registers to restore data in regV5. 2026-07-18 22:59:05 -04:00
Cody Henthorne 1f5d211b06 Fix translation in screenshot test. 2026-07-18 22:59:05 -04:00
Cody Henthorne 81926d1bc4 Bump version to 8.20.3 2026-07-17 16:14:00 -04:00
Cody Henthorne b327d92b82 Update translations and other static files. 2026-07-17 16:05:21 -04:00
Cody Henthorne 9a8e0dd64f Fix linked device changing discoverability bug. 2026-07-17 15:56:53 -04:00
Alex Hart c50757880e Add proper rounded corners and end margin to detail pane. 2026-07-17 15:56:53 -04:00
Alex Hart 99b1fa0ebb Update span count in gallery according to breakpoint. 2026-07-17 15:56:53 -04:00
Cody Henthorne 74f08927f2 Maybe mark registration complete after a terminal restore decision. 2026-07-17 15:56:53 -04:00
903 changed files with 21131 additions and 10533 deletions
+30 -27
View File
@@ -1,27 +1,30 @@
version: 2
updates:
# Automatically keep GitHub Actions SHA-pinned to the latest commit SHAs.
# Dependabot will update both the SHA and the inline version comment (e.g. # v6)
# while leaving any extra documentation comments intact.
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
labels:
- "dependencies"
commit-message:
prefix: "ci"
groups:
actions:
patterns:
- "actions/*"
gradle-actions:
patterns:
- "gradle/*"
peter-evans:
patterns:
- "peter-evans/*"
usefulness:
patterns:
- "usefulness/*"
# Dependabot automated schedule is currently disabled.
# To re-enable, uncomment the block below.
#
# version: 2
# updates:
# # Automatically keep GitHub Actions SHA-pinned to the latest commit SHAs.
# # Dependabot will update both the SHA and the inline version comment (e.g. # v6)
# # while leaving any extra documentation comments intact.
# - package-ecosystem: "github-actions"
# directory: "/"
# schedule:
# interval: "weekly"
# day: "monday"
# labels:
# - "dependencies"
# commit-message:
# prefix: "ci"
# groups:
# actions:
# patterns:
# - "actions/*"
# gradle-actions:
# patterns:
# - "gradle/*"
# peter-evans:
# patterns:
# - "peter-evans/*"
# usefulness:
# patterns:
# - "usefulness/*"
+2 -2
View File
@@ -16,14 +16,14 @@ jobs:
runs-on: ubuntu-latest-8-cores
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
# gh api repos/actions/checkout/commits/v6 --jq '.sha'
with:
submodules: true
lfs: true
- name: set up JDK 21
uses: actions/setup-java@0f481fcb613427c0f801b606911222b5b6f3083a # v5
uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5
# gh api repos/actions/setup-java/commits/v5 --jq '.sha'
with:
distribution: temurin
+3 -3
View File
@@ -16,14 +16,14 @@ jobs:
runs-on: ubuntu-latest-8-cores
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
# gh api repos/actions/checkout/commits/v6 --jq '.sha'
with:
submodules: true
ref: ${{ github.event.pull_request.base.sha }}
- name: set up JDK 21
uses: actions/setup-java@0f481fcb613427c0f801b606911222b5b6f3083a # v5
uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5
# gh api repos/actions/setup-java/commits/v5 --jq '.sha'
with:
distribution: temurin
@@ -61,7 +61,7 @@ jobs:
if: steps.cache-base.outputs.cache-hit != 'true'
run: mv app/build/outputs/apk/playProd/release/*arm64*.apk diffuse-base.apk
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
# gh api repos/actions/checkout/commits/v6 --jq '.sha'
with:
submodules: true
+33 -1
View File
@@ -11,7 +11,29 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Free up disk space
run: |
# The build runs entirely inside the Docker container, which ships its
# own JDK, Android SDK, and NDK. None of the host toolchains are needed.
sudo rm -rf /usr/lib/jvm
sudo rm -rf /usr/share/dotnet
sudo rm -rf /usr/share/swift
sudo rm -rf /usr/local/.ghcup
sudo rm -rf /usr/local/julia*
sudo rm -rf /usr/local/lib/android
sudo rm -rf /usr/local/share/chromium
sudo rm -rf /opt/microsoft /opt/google
sudo rm -rf /opt/az
sudo rm -rf /usr/local/share/powershell
sudo rm -rf /opt/hostedtoolcache
docker system prune -af || true
docker builder prune -af || true
- name: Disk usage after cleanup
if: always()
run: |
df -h
docker system df || true
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
# gh api repos/actions/checkout/commits/v6 --jq '.sha'
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
@@ -23,5 +45,15 @@ jobs:
load: true
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Disk usage after image build
if: always()
run: |
df -h
docker system df || true
- name: Test build
run: docker run --memory=12g --rm -v "$(pwd)":/project -w /project signal-android ./gradlew --no-daemon --max-workers=1 -Dorg.gradle.jvmargs="-Xmx4g -XX:MaxMetaspaceSize=512m" -Dkotlin.compiler.execution.strategy=in-process bundlePlayProdRelease
- name: Disk usage after test build
if: always()
run: |
df -h
docker system df || true
+15 -9
View File
@@ -32,8 +32,8 @@ plugins {
val staticIps = Properties().apply { file("static-ips.properties").reader().use { load(it) } }
staticIps.stringPropertyNames().forEach { rootProject.extra[it] = staticIps.getProperty(it) }
val canonicalVersionCode = 1722
val canonicalVersionName = "8.20.2"
val canonicalVersionCode = 1725
val canonicalVersionName = "8.21.0"
val currentHotfixVersion = 0
val maxHotfixVersions = 100
@@ -154,7 +154,11 @@ android {
experimentalProperties["android.experimental.enableScreenshotTest"] = true
buildToolsVersion = libs.versions.buildTools.get()
compileSdkVersion(libs.versions.compileSdk.get())
compileSdk {
version = release(libs.versions.compileSdk.get().toInt())
}
ndkVersion = libs.versions.ndk.get()
flavorDimensions += listOf("distribution", "environment")
@@ -191,12 +195,12 @@ android {
sourceSets {
getByName("test") {
java.srcDir("$projectDir/src/testShared")
java.directories += "$projectDir/src/testShared"
}
getByName("androidTest") {
java.srcDir("$projectDir/src/testShared")
java.srcDir("$projectDir/src/benchmarkShared/java")
java.directories += "$projectDir/src/testShared"
java.directories += "$projectDir/src/benchmarkShared/java"
}
}
@@ -411,6 +415,7 @@ android {
isDefault = false
isDebuggable = false
isMinifyEnabled = true
isShrinkResources = true
matchingFallbacks += "debug"
buildConfigField("String", "BUILD_VARIANT_TYPE", "\"Benchmark\"")
buildConfigField("boolean", "TRACING_ENABLED", "true")
@@ -521,18 +526,18 @@ android {
android.buildTypes.configureEach {
val path = if (name == "release") releaseDir else debugDir
sourceSets.named(name) {
java.srcDir(path)
java.directories += path
}
}
sourceSets {
getByName("mocked") {
java.srcDir("$projectDir/src/benchmarkShared/java")
java.directories += "$projectDir/src/benchmarkShared/java"
manifest.srcFile("$projectDir/src/benchmarkShared/AndroidManifest.xml")
}
getByName("benchmark") {
java.srcDir("$projectDir/src/benchmarkShared/java")
java.directories += "$projectDir/src/benchmarkShared/java"
manifest.srcFile("$projectDir/src/benchmarkShared/AndroidManifest.xml")
}
}
@@ -698,6 +703,7 @@ dependencies {
implementation(project(":core:ui"))
implementation(project(":core:models"))
implementation(project(":core:models-jvm"))
implementation(project(":core:serialization"))
implementation(project(":feature:camera"))
implementation(project(":feature:registration"))
implementation(project(":lib:apng"))
@@ -30,6 +30,7 @@ import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.app.subscription.permits.DonationPermits
import org.thoughtcrime.securesms.database.InAppPaymentTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.testing.DisableAnimationsRule
import org.thoughtcrime.securesms.testing.GooglePayTestRule
import org.thoughtcrime.securesms.testing.InAppPaymentsRule
import org.thoughtcrime.securesms.testing.RxTestSchedulerRule
@@ -54,6 +55,9 @@ class CheckoutFlowActivityTest__OneTimeDonations {
@get:Rule
val composeRule = createEmptyComposeRule()
@get:Rule
val animationsRule = DisableAnimationsRule()
private val intent = CheckoutFlowActivity.createIntent(InstrumentationRegistry.getInstrumentation().targetContext, InAppPaymentType.ONE_TIME_DONATION)
@Before
@@ -29,6 +29,7 @@ import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.testing.DisableAnimationsRule
import org.thoughtcrime.securesms.testing.GooglePayTestRule
import org.thoughtcrime.securesms.testing.InAppPaymentsRule
import org.thoughtcrime.securesms.testing.RxTestSchedulerRule
@@ -59,6 +60,9 @@ class CheckoutFlowActivityTest__RecurringDonations {
@get:Rule
val composeRule = createEmptyComposeRule()
@get:Rule
val animationsRule = DisableAnimationsRule()
private val intent = CheckoutFlowActivity.createIntent(InstrumentationRegistry.getInstrumentation().targetContext, InAppPaymentType.RECURRING_DONATION)
@Before
@@ -26,6 +26,7 @@ import org.thoughtcrime.securesms.database.DonationReceiptTable
import org.thoughtcrime.securesms.database.InAppPaymentTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.InAppPaymentReceiptRecord
import org.thoughtcrime.securesms.testing.DisableAnimationsRule
import org.thoughtcrime.securesms.testing.GooglePayTestRule
import org.thoughtcrime.securesms.testing.InAppPaymentsRule
import org.thoughtcrime.securesms.testing.RxTestSchedulerRule
@@ -59,6 +60,9 @@ class CheckoutFlowActivityTest__Redemption {
@get:Rule
val composeRule = createEmptyComposeRule()
@get:Rule
val animationsRule = DisableAnimationsRule()
private val intent = CheckoutFlowActivity.createIntent(InstrumentationRegistry.getInstrumentation().targetContext, InAppPaymentType.ONE_TIME_DONATION)
@Before
@@ -27,6 +27,7 @@ import org.thoughtcrime.securesms.database.DonationReceiptTable
import org.thoughtcrime.securesms.database.InAppPaymentTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.InAppPaymentReceiptRecord
import org.thoughtcrime.securesms.testing.DisableAnimationsRule
import org.thoughtcrime.securesms.testing.GooglePayTestRule
import org.thoughtcrime.securesms.testing.InAppPaymentsRule
import org.thoughtcrime.securesms.testing.RawFlag
@@ -69,6 +70,9 @@ class DonationsCheckoutSpecTest(private val spec: DonationsCheckoutTestSpec) {
@get:Rule
val composeRule = createEmptyComposeRule()
@get:Rule
val animationsRule = DisableAnimationsRule()
@Before
fun setUp() {
SignalDatabase.inAppPayments.writableDatabase.deleteAll(InAppPaymentTable.TABLE_NAME)
@@ -9,6 +9,7 @@ import org.signal.core.util.billing.BillingApi
import org.signal.libsignal.net.Network
import org.signal.libsignal.zkgroup.receipts.ClientZkReceiptOperations
import org.signal.network.api.ArchiveApi
import org.signal.network.config.SignalServiceConfiguration
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess
import org.thoughtcrime.securesms.recipients.LiveRecipientCache
import org.thoughtcrime.securesms.testing.endpoints.DonationTestServer
@@ -21,7 +22,6 @@ import org.whispersystems.signalservice.api.account.AccountApi
import org.whispersystems.signalservice.api.keys.KeysApi
import org.whispersystems.signalservice.api.message.MessageApi
import org.whispersystems.signalservice.api.websocket.SignalWebSocket
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration
import org.whispersystems.signalservice.internal.push.PushServiceSocket
import java.util.function.Supplier
import kotlin.time.Duration.Companion.seconds
@@ -101,7 +101,7 @@ class DataMessageProcessorTest_polls {
message = DataMessage(pollTerminate = DataMessage.PollTerminate(targetSentTimestamp = 100)),
senderRecipient = alice,
metadata = EnvelopeMetadata(alice.requireServiceId(), null, 1, false, null, harness.self.requireServiceId(), CiphertextMessage.WHISPER_TYPE),
threadRecipient = bob,
threadRecipient = Recipient.resolved(groupRecipientId),
groupId = groupId,
receivedTime = 200
)
@@ -310,6 +310,7 @@ class DataMessageProcessorTest_polls {
envelope = MessageContentFuzzer.envelope(100),
message = DataMessage(pollVote = pollVote),
senderRecipient = senderRecipient,
threadRecipient = Recipient.resolved(groupRecipientId),
earlyMessageCacheEntry = null
)
}
@@ -0,0 +1,49 @@
/*
* Copyright 2026 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.testing
import android.provider.Settings
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.rules.ExternalResource
import java.io.FileInputStream
/**
* Disables system animation scales for the duration of a test and restores them afterward.
*
* Espresso and [android.app.Instrumentation.waitForIdleSync] only make progress once the main looper is idle. An
* on-screen indeterminate animation (e.g. the checkout's `CircularProgressIndicator`) posts frame callbacks forever, so
* the looper never idles and any idle-gated wait hangs indefinitely rather than timing out. Forcing the scales to 0
* stops those animations so the looper can idle.
*
* Writes go through the shell (`UiAutomation`), which holds `WRITE_SECURE_SETTINGS`; the app process does not.
*/
class DisableAnimationsRule : ExternalResource() {
private val scales = listOf(
Settings.Global.WINDOW_ANIMATION_SCALE,
Settings.Global.TRANSITION_ANIMATION_SCALE,
Settings.Global.ANIMATOR_DURATION_SCALE
)
private lateinit var previous: Map<String, Float>
override fun before() {
val resolver = InstrumentationRegistry.getInstrumentation().targetContext.contentResolver
previous = scales.associateWith { Settings.Global.getFloat(resolver, it, 1f) }
scales.forEach { putScale(it, 0f) }
}
override fun after() {
if (::previous.isInitialized) {
previous.forEach { (key, value) -> putScale(key, value) }
}
}
private fun putScale(key: String, value: Float) {
val stream = InstrumentationRegistry.getInstrumentation().uiAutomation.executeShellCommand("settings put global $key $value")
FileInputStream(stream.fileDescriptor).use { it.readBytes() }
}
}
@@ -4,13 +4,13 @@ import android.app.Application
import org.signal.benchmark.setup.NoOpJob
import org.signal.core.util.UptimeSleepTimer
import org.signal.libsignal.net.Network
import org.signal.network.config.SignalServiceConfiguration
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.dependencies.ApplicationDependencyProvider
import org.thoughtcrime.securesms.dependencies.InstrumentationApplicationDependencyProvider
import org.thoughtcrime.securesms.jobmanager.JobManager
import org.thoughtcrime.securesms.jobs.JobManagerFactories
import org.whispersystems.signalservice.api.websocket.SignalWebSocket
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration
import org.whispersystems.signalservice.internal.websocket.BenchmarkWebSocketConnection
import java.util.function.Supplier
import kotlin.time.Duration.Companion.seconds
@@ -9,13 +9,13 @@ import android.app.Application
import org.signal.benchmark.setup.NoOpJob
import org.signal.core.util.UptimeSleepTimer
import org.signal.libsignal.net.Network
import org.signal.network.config.SignalServiceConfiguration
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.dependencies.ApplicationDependencyProvider
import org.thoughtcrime.securesms.jobmanager.JobManager
import org.thoughtcrime.securesms.jobs.JobManagerFactories
import org.thoughtcrime.securesms.net.DeviceTransferBlockingInterceptor
import org.whispersystems.signalservice.api.websocket.SignalWebSocket
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration
import org.whispersystems.signalservice.internal.websocket.BenchmarkWebSocketConnection
import java.util.function.Supplier
import kotlin.time.Duration.Companion.seconds
+10 -2
View File
@@ -469,18 +469,18 @@
<activity
android:name=".mediasend.v2.MediaSelectionActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"
android:screenOrientation="portrait"
android:exported="false"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:theme="@style/TextSecure.DarkNoActionBar"
android:windowSoftInputMode="stateAlwaysHidden|adjustNothing" />
<activity
android:name=".mediasend.v3.MediaSendV3Activity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"
android:screenOrientation="portrait"
android:exported="false"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:theme="@style/Signal.DayNight.NoActionBar"
android:windowSoftInputMode="stateAlwaysHidden|adjustNothing" />
@@ -892,6 +892,14 @@
android:launchMode="singleTask"
android:theme="@style/Theme.Signal.DayNight.NoActionBar" />
<activity
android:name=".clockskew.ClockSkewActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:excludeFromRecents="true"
android:exported="false"
android:launchMode="singleTask"
android:theme="@style/Theme.Signal.DayNight.NoActionBar" />
<activity
android:name=".ratelimit.RecaptchaProofActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -52,6 +52,7 @@ import org.signal.ringrtc.CallManager;
import org.thoughtcrime.securesms.apkupdate.ApkUpdateRefreshListener;
import org.thoughtcrime.securesms.avatar.AvatarPickerStorage;
import org.thoughtcrime.securesms.backup.v2.BackupRepository;
import org.thoughtcrime.securesms.clockskew.ClockSkewDetector;
import org.thoughtcrime.securesms.preferences.EditProxyActivity;
import org.thoughtcrime.securesms.conversation.drafts.DraftBlobs;
import org.thoughtcrime.securesms.crypto.AppAttachmentSecretStore;
@@ -420,6 +421,7 @@ public class ApplicationContext extends Application implements AppForegroundObse
AppDependencies.init(this, new ApplicationDependencyProvider(this));
}
AppForegroundObserver.begin();
ClockSkewDetector.beginObserving(this);
if (Environment.USE_NEW_REGISTRATION) {
initializeRegistrationDependencies();
@@ -429,7 +431,7 @@ public class ApplicationContext extends Application implements AppForegroundObse
private void initializeRegistrationDependencies() {
RegistrationDependencies.provide(
new RegistrationDependencies(
new AppRegistrationNetworkController(this, AppDependencies.getPushServiceSocket()),
new AppRegistrationNetworkController(this, AppDependencies.getRegistrationApiV2()),
new AppRegistrationStorageController(this),
Environment.IS_LINK_AND_SYNC_AVAILABLE,
null,
@@ -36,6 +36,7 @@ import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.FullscreenHelper;
import org.thoughtcrime.securesms.util.WindowUtil;
/**
* Activity for displaying avatars full screen.
@@ -77,6 +78,9 @@ public final class AvatarPreviewActivity extends PassphraseRequiredActivity {
setTheme(R.style.TextSecure_MediaPreview);
setContentView(R.layout.contact_photo_preview_activity);
WindowUtil.clearLightStatusBar(getWindow());
WindowUtil.clearLightNavigationBar(getWindow());
postponeEnterTransition();
TransitionInflater inflater = TransitionInflater.from(this);
getWindow().setSharedElementEnterTransition(inflater.inflateTransition(R.transition.full_screen_avatar_image_enter_transition_set));
@@ -6,6 +6,7 @@ import android.content.res.Configuration;
import android.os.Bundle;
import android.view.View;
import androidx.activity.EdgeToEdge;
import androidx.annotation.NonNull;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
@@ -34,6 +35,7 @@ public abstract class BaseActivity extends AppCompatActivity {
protected void onCreate(Bundle savedInstanceState) {
AppStartup.getInstance().onCriticalRenderEventStart();
logEvent("onCreate()");
EdgeToEdge.enable(this);
super.onCreate(savedInstanceState);
AppStartup.getInstance().onCriticalRenderEventEnd();
}
@@ -19,9 +19,11 @@ package org.thoughtcrime.securesms;
import android.content.Context;
import android.os.AsyncTask;
import android.os.Bundle;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.appcompat.widget.Toolbar;
import androidx.core.view.WindowInsetsCompat;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import org.signal.core.util.DimensionUnit;
@@ -34,6 +36,7 @@ import org.thoughtcrime.securesms.contacts.sync.ContactDiscovery;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.SystemWindowInsetsSetter;
import java.io.IOException;
import java.lang.ref.WeakReference;
@@ -110,11 +113,16 @@ public abstract class ContactSelectionActivity extends PassphraseRequiredActivit
getSupportActionBar().setDisplayHomeAsUpEnabled(false);
getSupportActionBar().setIcon(null);
getSupportActionBar().setLogo(null);
toolbar.getLayoutParams().height = ViewGroup.LayoutParams.WRAP_CONTENT;
SystemWindowInsetsSetter.attach(toolbar, this, WindowInsetsCompat.Type.statusBars());
}
private void initializeResources() {
contactsFragment = (ContactSelectionListFragment) getSupportFragmentManager().findFragmentById(R.id.contact_selection_list_fragment);
contactsFragment.setOnRefreshListener(this);
SystemWindowInsetsSetter.attach(contactsFragment.requireView(), this, WindowInsetsCompat.Type.navigationBars());
}
private void initializeSearch() {
@@ -672,46 +672,54 @@ class MainActivity :
}
},
primaryContent = {
when (mainNavigationState.currentListLocation) {
MainNavigationListLocation.CHATS, MainNavigationListLocation.ARCHIVE -> {
NavDisplay<NavKey>(
backStack = mainNavigationViewModel.chatsBackStackEntries,
onBack = { mainNavigationViewModel.popChatsDetailLocation() },
transitionSpec = { TransitionSpecs.HorizontalSlide.transitionSpec },
popTransitionSpec = { TransitionSpecs.HorizontalSlide.popTransitionSpec },
predictivePopTransitionSpec = { TransitionSpecs.HorizontalSlide.predictivePopTransitionSpec },
entryProvider = entryProvider { chatsNavEntries(convoTransitionState) }
)
}
Box(
modifier = Modifier
.padding(end = contentLayoutData.detailPaddingEnd)
.clip(contentLayoutData.shape)
.background(color = MaterialTheme.colorScheme.surface)
.fillMaxSize()
) {
when (mainNavigationState.currentListLocation) {
MainNavigationListLocation.CHATS, MainNavigationListLocation.ARCHIVE -> {
NavDisplay<NavKey>(
backStack = mainNavigationViewModel.chatsBackStackEntries,
onBack = { mainNavigationViewModel.popChatsDetailLocation() },
transitionSpec = { TransitionSpecs.HorizontalSlide.transitionSpec },
popTransitionSpec = { TransitionSpecs.HorizontalSlide.popTransitionSpec },
predictivePopTransitionSpec = { TransitionSpecs.HorizontalSlide.predictivePopTransitionSpec },
entryProvider = entryProvider { chatsNavEntries(convoTransitionState) }
)
}
MainNavigationListLocation.CALLS -> {
NavDisplay<NavKey>(
backStack = mainNavigationViewModel.callsBackStackEntries,
onBack = { mainNavigationViewModel.popCallsDetailLocation() },
transitionSpec = { TransitionSpecs.HorizontalSlide.transitionSpec },
popTransitionSpec = { TransitionSpecs.HorizontalSlide.popTransitionSpec },
predictivePopTransitionSpec = { TransitionSpecs.HorizontalSlide.predictivePopTransitionSpec },
entryDecorators = listOf(
rememberSaveableStateHolderNavEntryDecorator(),
rememberViewModelStoreNavEntryDecorator()
),
entryProvider = entryProvider { callsNavEntries(isSplitPane) }
)
}
MainNavigationListLocation.CALLS -> {
NavDisplay<NavKey>(
backStack = mainNavigationViewModel.callsBackStackEntries,
onBack = { mainNavigationViewModel.popCallsDetailLocation() },
transitionSpec = { TransitionSpecs.HorizontalSlide.transitionSpec },
popTransitionSpec = { TransitionSpecs.HorizontalSlide.popTransitionSpec },
predictivePopTransitionSpec = { TransitionSpecs.HorizontalSlide.predictivePopTransitionSpec },
entryDecorators = listOf(
rememberSaveableStateHolderNavEntryDecorator(),
rememberViewModelStoreNavEntryDecorator()
),
entryProvider = entryProvider { callsNavEntries(isSplitPane) }
)
}
MainNavigationListLocation.STORIES -> {
NavDisplay<NavKey>(
backStack = mainNavigationViewModel.storiesBackStackEntries,
onBack = { mainNavigationViewModel.popStoriesDetailLocation() },
transitionSpec = { TransitionSpecs.HorizontalSlide.transitionSpec },
popTransitionSpec = { TransitionSpecs.HorizontalSlide.popTransitionSpec },
predictivePopTransitionSpec = { TransitionSpecs.HorizontalSlide.predictivePopTransitionSpec },
entryDecorators = listOf(
rememberSaveableStateHolderNavEntryDecorator(),
rememberViewModelStoreNavEntryDecorator()
),
entryProvider = entryProvider { storiesNavEntries() }
)
MainNavigationListLocation.STORIES -> {
NavDisplay<NavKey>(
backStack = mainNavigationViewModel.storiesBackStackEntries,
onBack = { mainNavigationViewModel.popStoriesDetailLocation() },
transitionSpec = { TransitionSpecs.HorizontalSlide.transitionSpec },
popTransitionSpec = { TransitionSpecs.HorizontalSlide.popTransitionSpec },
predictivePopTransitionSpec = { TransitionSpecs.HorizontalSlide.predictivePopTransitionSpec },
entryDecorators = listOf(
rememberSaveableStateHolderNavEntryDecorator(),
rememberViewModelStoreNavEntryDecorator()
),
entryProvider = entryProvider { storiesNavEntries() }
)
}
}
}
},
@@ -966,7 +974,6 @@ class MainActivity :
onSetToolbarColor = {
toolbarViewModel.setToolbarColor(it)
},
setStatusBarColor = {},
lifecycleOwner = lifecycleOwner
).attach(recyclerView)
}
@@ -976,7 +983,6 @@ class MainActivity :
activity = this,
views = listOf(chatFolders),
viewStubs = listOf(),
setStatusBarColor = {},
onSetToolbarColor = {
toolbarViewModel.setToolbarColor(it)
},
@@ -18,6 +18,8 @@ import org.signal.core.util.logging.Log;
import org.signal.core.util.tracing.Tracer;
import org.signal.devicetransfer.TransferStatus;
import org.signal.registration.RegistrationRoute;
import org.thoughtcrime.securesms.clockskew.ClockSkewActivity;
import org.thoughtcrime.securesms.clockskew.ClockSkewDetector;
import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNumberLockActivity;
import org.thoughtcrime.securesms.crypto.MasterSecretUtil;
import org.thoughtcrime.securesms.dependencies.AppDependencies;
@@ -59,6 +61,7 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
private static final int STATE_CHANGE_NUMBER_LOCK = 10;
private static final int STATE_TRANSFER_OR_RESTORE = 11;
private static final int STATE_RESUME_LINKING_REG = 12;
private static final int STATE_CLOCK_SKEW = 13;
private SignalServiceNetworkAccess networkAccess;
private BroadcastReceiver clearKeyReceiver;
@@ -158,6 +161,7 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
case STATE_CHANGE_NUMBER_LOCK: return getChangeNumberLockIntent();
case STATE_TRANSFER_OR_RESTORE: return getTransferOrRestoreIntent();
case STATE_RESUME_LINKING_REG: return getResumeLinkedRegistrationIntent();
case STATE_CLOCK_SKEW: return getClockSkewIntent();
default: return null;
}
}
@@ -187,6 +191,8 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
return STATE_TRANSFER_LOCKED;
} else if (SignalStore.misc().isChangeNumberLocked() && getClass() != ChangeNumberLockActivity.class) {
return STATE_CHANGE_NUMBER_LOCK;
} else if (ClockSkewDetector.INSTANCE.isDetected() && getClass() != ClockSkewActivity.class) {
return STATE_CLOCK_SKEW;
} else {
return STATE_NORMAL;
}
@@ -288,6 +294,10 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
return ChangeNumberLockActivity.createIntent(this);
}
private Intent getClockSkewIntent() {
return ClockSkewActivity.createIntent(this);
}
private Intent getRoutedIntent(Intent destination, @Nullable Intent nextIntent) {
if (nextIntent != null) destination.putExtra(NEXT_INTENT_EXTRA, nextIntent);
return destination;
@@ -63,6 +63,7 @@ class TextAvatarCreationFragment : Fragment(R.layout.text_avatar_creation_fragme
val tabLayout: ControllableTabLayout = view.findViewById(R.id.text_avatar_creation_tabs)
val doneButton: View = view.findViewById(R.id.text_avatar_creation_done)
val keyboardAwareLayout: KeyboardAwareLinearLayout = view.findViewById(R.id.keyboard_aware_layout)
keyboardAwareLayout.setInsetPaddingMode(KeyboardAwareLinearLayout.InsetPaddingMode.SAFE_AREA)
withRecyclerSet.load(requireContext(), R.layout.text_avatar_creation_fragment_content)
withoutRecyclerSet.load(requireContext(), R.layout.text_avatar_creation_fragment_content_hidden_recycler)
@@ -46,7 +46,7 @@ import org.thoughtcrime.securesms.database.SenderKeyTable;
import org.thoughtcrime.securesms.database.SenderKeySharedTable;
import org.thoughtcrime.securesms.database.SessionTable;
import org.thoughtcrime.securesms.database.SignedPreKeyTable;
import org.thoughtcrime.securesms.database.StickerTable;
import org.thoughtcrime.securesms.database.StickerTables;
import org.thoughtcrime.securesms.database.model.AvatarPickerDatabase;
import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.keyvalue.KeyValueDataSet;
@@ -186,7 +186,7 @@ public class FullBackupExporter extends FullBackupBase {
count = exportTable(table, input, outputStream, cursor -> isForNonExpiringPollMessage(input, cursor.getLong(cursor.getColumnIndexOrThrow(PollTables.PollOptionTable.POLL_ID))), null, count, estimatedCount, cancellationSignal);
} else if (table.equals(PollTables.PollVoteTable.TABLE_NAME)) {
count = exportTable(table, input, outputStream, cursor -> isForNonExpiringPollMessage(input, cursor.getLong(cursor.getColumnIndexOrThrow(PollTables.PollVoteTable.POLL_ID))), null, count, estimatedCount, cancellationSignal);
} else if (table.equals(StickerTable.TABLE_NAME)) {
} else if (table.equals(StickerTables.Sticker.TABLE_NAME)) {
count = exportTable(table, input, outputStream, cursor -> true, (cursor, innerCount) -> exportSticker(attachmentSecret, cursor, outputStream, innerCount, estimatedCount), count, estimatedCount, cancellationSignal);
} else if (!TABLE_CONTENT_BLOCKLIST.contains(table)) {
count = exportTable(table, input, outputStream, null, null, count, estimatedCount, cancellationSignal);
@@ -238,7 +238,7 @@ public class FullBackupExporter extends FullBackupBase {
count += getCount(input, BackupCountQueries.getGroupReceiptCount());
} else if (table.equals(AttachmentTable.TABLE_NAME)) {
count += getCount(input, BackupCountQueries.getAttachmentCount());
} else if (table.equals(StickerTable.TABLE_NAME)) {
} else if (table.equals(StickerTables.Sticker.TABLE_NAME)) {
count += getCount(input, "SELECT COUNT(*) FROM " + table);
} else if (!TABLE_CONTENT_BLOCKLIST.contains(table)) {
count += getCount(input, "SELECT COUNT(*) FROM " + table);
@@ -497,11 +497,11 @@ public class FullBackupExporter extends FullBackupBase {
long estimatedCount)
throws IOException
{
long rowId = cursor.getLong(cursor.getColumnIndexOrThrow(StickerTable.ID));
long size = cursor.getLong(cursor.getColumnIndexOrThrow(StickerTable.FILE_LENGTH));
long rowId = cursor.getLong(cursor.getColumnIndexOrThrow(StickerTables.Sticker.ID));
long size = cursor.getLong(cursor.getColumnIndexOrThrow(StickerTables.Sticker.FILE_LENGTH));
String data = cursor.getString(cursor.getColumnIndexOrThrow(StickerTable.FILE_PATH));
byte[] random = cursor.getBlob(cursor.getColumnIndexOrThrow(StickerTable.FILE_RANDOM));
String data = cursor.getString(cursor.getColumnIndexOrThrow(StickerTables.Sticker.FILE_PATH));
byte[] random = cursor.getBlob(cursor.getColumnIndexOrThrow(StickerTables.Sticker.FILE_RANDOM));
if (!TextUtils.isEmpty(data) && size > 0) {
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count, estimatedCount));
@@ -34,7 +34,7 @@ import org.thoughtcrime.securesms.database.LastResortKeyTupleTable;
import org.thoughtcrime.securesms.database.OneTimePreKeyTable;
import org.thoughtcrime.securesms.database.SearchTable;
import org.thoughtcrime.securesms.database.SignedPreKeyTable;
import org.thoughtcrime.securesms.database.StickerTable;
import org.thoughtcrime.securesms.database.StickerTables;
import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.keyvalue.KeyValueDataSet;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
@@ -244,7 +244,7 @@ public class FullBackupImporter extends FullBackupBase {
private static void processSticker(@NonNull Context context, @NonNull AttachmentSecret attachmentSecret, @NonNull SQLiteDatabase db, @NonNull Sticker sticker, BackupRecordInputStream inputStream)
throws IOException
{
File stickerDirectory = context.getDir(StickerTable.DIRECTORY, Context.MODE_PRIVATE);
File stickerDirectory = context.getDir(StickerTables.DIRECTORY, Context.MODE_PRIVATE);
File dataFile = File.createTempFile("sticker", ".mms", stickerDirectory);
Pair<byte[], OutputStream> output = ModernEncryptingPartOutputStream.createFor(attachmentSecret, dataFile, false);
@@ -252,12 +252,12 @@ public class FullBackupImporter extends FullBackupBase {
inputStream.readAttachmentTo(output.getSecond(), sticker.length);
ContentValues contentValues = new ContentValues();
contentValues.put(StickerTable.FILE_PATH, dataFile.getAbsolutePath());
contentValues.put(StickerTable.FILE_LENGTH, sticker.length);
contentValues.put(StickerTable.FILE_RANDOM, output.getFirst());
contentValues.put(StickerTables.Sticker.FILE_PATH, dataFile.getAbsolutePath());
contentValues.put(StickerTables.Sticker.FILE_LENGTH, sticker.length);
contentValues.put(StickerTables.Sticker.FILE_RANDOM, output.getFirst());
db.update(StickerTable.TABLE_NAME, contentValues,
StickerTable.ID + " = ?",
db.update(StickerTables.Sticker.TABLE_NAME, contentValues,
StickerTables.Sticker.ID + " = ?",
new String[] {String.valueOf(sticker.rowId)});
}
@@ -105,7 +105,7 @@ import org.thoughtcrime.securesms.database.SearchTable
import org.thoughtcrime.securesms.database.SessionTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.SignedPreKeyTable
import org.thoughtcrime.securesms.database.StickerTable
import org.thoughtcrime.securesms.database.StickerTables
import org.thoughtcrime.securesms.database.ThreadTable
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
import org.thoughtcrime.securesms.dependencies.AppDependencies
@@ -1338,7 +1338,7 @@ object BackupRepository {
SignalDatabase.recipients.setProfileKey(selfId, selfData.profileKey)
SignalDatabase.recipients.setProfileSharing(selfId, true)
val importState = ImportState(mediaRootBackupKey)
val importState = ImportState(mediaRootBackupKey, backupMode)
val chatItemInserter: ChatItemArchiveImporter = ChatItemArchiveProcessor.beginImport(importState)
Log.d(TAG, "[import] Beginning to read frames.")
@@ -1487,7 +1487,7 @@ object BackupRepository {
}
val stickerJobs = SignalDatabase.stickers.getAllStickerPacks().use { cursor ->
val reader = StickerTable.StickerPackRecordReader(cursor)
val reader = StickerTables.StickerPackRecordReader(cursor)
reader
.filter { it.isInstalled }
.map {
@@ -1522,7 +1522,7 @@ object BackupRepository {
val jobs = mutableListOf<Job>()
groups
.asSequence()
.filter { it.id.isV2 }
.filter { it.id.isV2 && it.hasV2GroupProperties }
.forEach { group ->
jobs.add(RequestGroupV2InfoJob(group.id as GroupId.V2))
val avatarKey = group.requireV2GroupProperties().avatarKey
@@ -2530,7 +2530,7 @@ class ExportState(
var releaseNoteRecipientId: Long? = null
}
class ImportState(val mediaRootBackupKey: MediaRootBackupKey) {
class ImportState(val mediaRootBackupKey: MediaRootBackupKey, val backupMode: BackupMode) {
val remoteToLocalRecipientId: MutableMap<Long, RecipientId> = hashMapOf()
val chatIdToLocalThreadId: MutableMap<Long, Long> = hashMapOf()
val chatIdToLocalRecipientId: MutableMap<Long, RecipientId> = hashMapOf()
@@ -104,11 +104,23 @@ class ArchiveFileSystem private constructor(private val context: Context, root:
}
/**
* Returns true if [dir] appears to be a SignalBackups directory based on its name and
* expected internal structure (presence of the "files" subdirectory).
* Returns true if [dir] appears to be a SignalBackups directory based on its expected internal
* structure. We can't rely on the directory being named [MAIN_DIRECTORY_NAME], since users may
* rename it or restore it into a differently-named folder (e.g. when transferring a backup
* between devices). A directory is therefore considered a backups directory if it either matches
* the expected name or directly contains the archive contents (a "files" subdirectory alongside
* at least one snapshot directory).
*/
private fun looksLikeSignalBackupsDirectory(dir: DocumentFile): Boolean {
return dir.name == MAIN_DIRECTORY_NAME && dir.findFile("files") != null
if (dir.findFile("files") == null) {
return false
}
if (dir.name == MAIN_DIRECTORY_NAME) {
return true
}
return dir.listFiles().any { it.isDirectory && it.name?.startsWith(BACKUP_DIRECTORY_PREFIX) == true }
}
/**
@@ -275,7 +275,7 @@ object AccountDataArchiveProcessor {
SignalStore.story.isFeatureDisabled = settings.storiesDisabled
SignalStore.story.userHasSeenGroupStoryEducationSheet = settings.hasSeenGroupStoryEducationSheet
SignalStore.story.viewedReceiptsEnabled = settings.storyViewReceiptsEnabled ?: settings.readReceipts
SignalStore.backup.optimizeStorage = settings.optimizeOnDeviceStorage
SignalStore.backup.optimizeStorage = !importState.backupMode.isLinkAndSync && settings.optimizeOnDeviceStorage
SignalStore.backup.backupTier = MessageBackupTier.fromBackupLevel(settings.backupTier)
SignalStore.settings.sentMediaQuality = settings.defaultSentMediaQuality.toLocalSentMediaQuality()
SignalStore.settings.setTheme(settings.appTheme.toLocalTheme())
@@ -10,13 +10,10 @@ import org.signal.archive.proto.Frame
import org.signal.archive.proto.StickerPack
import org.signal.archive.stream.BackupFrameEmitter
import org.signal.core.util.Hex
import org.signal.core.util.insertInto
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.backup.v2.ExportSkips
import org.thoughtcrime.securesms.database.SQLiteDatabase
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.StickerTable
import org.thoughtcrime.securesms.database.StickerTable.StickerPackRecordReader
import org.thoughtcrime.securesms.database.StickerTables.StickerPackRecordReader
import org.thoughtcrime.securesms.database.model.StickerPackRecord
import java.io.IOException
@@ -27,7 +24,7 @@ private val TAG = Log.tag(StickerArchiveProcessor::class)
*/
object StickerArchiveProcessor {
fun export(db: SignalDatabase, emitter: BackupFrameEmitter) {
StickerPackRecordReader(db.stickerTable.getAllStickerPacks()).use { reader ->
StickerPackRecordReader(db.stickerTables.getAllStickerPacks()).use { reader ->
var record: StickerPackRecord? = null
while (reader.getNext()?.let { record = it } != null) {
if (record!!.isInstalled) {
@@ -39,20 +36,10 @@ object StickerArchiveProcessor {
}
fun import(stickerPack: StickerPack) {
SignalDatabase.rawDatabase
.insertInto(StickerTable.TABLE_NAME)
.values(
StickerTable.PACK_ID to Hex.toStringCondensed(stickerPack.packId.toByteArray()),
StickerTable.PACK_KEY to Hex.toStringCondensed(stickerPack.packKey.toByteArray()),
StickerTable.PACK_TITLE to "",
StickerTable.PACK_AUTHOR to "",
StickerTable.INSTALLED to 1,
StickerTable.COVER to 1,
StickerTable.EMOJI to "",
StickerTable.CONTENT_TYPE to "",
StickerTable.FILE_PATH to ""
)
.run(SQLiteDatabase.CONFLICT_IGNORE)
SignalDatabase.stickers.insertPackReference(
packId = Hex.toStringCondensed(stickerPack.packId.toByteArray()),
packKey = Hex.toStringCondensed(stickerPack.packKey.toByteArray())
)
}
}
@@ -7,8 +7,6 @@ package org.thoughtcrime.securesms.backup.v2.ui.subscription
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContract
import androidx.core.content.IntentCompat
import androidx.core.os.bundleOf
@@ -42,12 +40,6 @@ class MessageBackupsCheckoutActivity : FragmentWrapperActivity() {
}
}
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
super.onCreate(savedInstanceState, ready)
enableEdgeToEdge()
}
override fun getFragment(): Fragment = MessageBackupsFlowFragment.create(
IntentCompat.getSerializableExtra(intent, TIER, MessageBackupTier::class.java)
)
@@ -52,14 +52,14 @@ import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.Scaffolds
import org.signal.core.ui.compose.SignalIcons
import org.signal.core.ui.compose.theme.SignalTheme
import org.signal.core.ui.fonts.SignalSymbols
import org.signal.core.ui.fonts.SignalSymbols.signalSymbolText
import org.signal.core.util.ByteUnit
import org.signal.core.util.billing.BillingResponseCode
import org.signal.core.util.bytes
import org.signal.core.util.money.FiatMoney
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
import org.thoughtcrime.securesms.fonts.SignalSymbols
import org.thoughtcrime.securesms.fonts.SignalSymbols.signalSymbolText
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
import java.math.BigDecimal
import java.util.Currency
@@ -5,7 +5,6 @@ import android.content.Intent
import android.os.Bundle
import android.widget.Toast
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
@@ -49,8 +48,6 @@ class VerifyBackupKeyActivity : PassphraseRequiredActivity() {
}
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
enableEdgeToEdge()
setContent {
SignalTheme {
val context = LocalContext.current
@@ -21,6 +21,7 @@ import org.signal.donations.InAppPaymentType
import org.thoughtcrime.securesms.MainActivity
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.InputAwareLayout
import org.thoughtcrime.securesms.components.KeyboardAwareLinearLayout
import org.thoughtcrime.securesms.components.emoji.EmojiEventListener
import org.thoughtcrime.securesms.components.emoji.MediaKeyboard
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
@@ -94,6 +95,7 @@ class GiftFlowConfirmationFragment :
.create()
inputAwareLayout = requireView().findViewById(R.id.input_aware_layout)
inputAwareLayout.setInsetPaddingMode(KeyboardAwareLinearLayout.InsetPaddingMode.KEYBOARD_AND_NAVIGATION_BAR)
emojiKeyboard = requireView().findViewById(R.id.emoji_drawer)
emojiKeyboard.setFragmentManager(childFragmentManager)
@@ -4,11 +4,13 @@ import android.content.Intent;
import android.graphics.Color;
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.Toolbar;
import androidx.core.view.WindowInsetsCompat;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
@@ -28,6 +30,7 @@ import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.signal.core.util.concurrent.LifecycleDisposable;
import org.thoughtcrime.securesms.util.SystemWindowInsetsSetter;
import org.thoughtcrime.securesms.util.ViewUtil;
import java.util.Optional;
@@ -63,6 +66,11 @@ public class BlockedUsersActivity extends PassphraseRequiredActivity implements
container = findViewById(R.id.fragment_container);
toolbar.setNavigationOnClickListener(unused -> onBackPressed());
toolbar.getLayoutParams().height = ViewGroup.LayoutParams.WRAP_CONTENT;
SystemWindowInsetsSetter.attach(toolbar, this, WindowInsetsCompat.Type.statusBars());
SystemWindowInsetsSetter.attach(container, this, WindowInsetsCompat.Type.navigationBars());
contactFilterView.setOnFilterChangedListener(query -> {
Fragment fragment = getSupportFragmentManager().findFragmentByTag(CONTACT_SELECTION_FRAGMENT);
if (fragment != null) {
@@ -11,7 +11,9 @@ import android.view.WindowManager
import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
@@ -43,6 +45,7 @@ import org.signal.core.ui.compose.Buttons
import org.signal.core.ui.compose.ComposeDialogFragment
import org.signal.core.ui.compose.Scaffolds
import org.signal.core.ui.compose.SignalIcons
import org.signal.core.ui.enableEdgeToEdge
import org.signal.core.ui.rememberIsSplitPane
import org.signal.core.util.BreakIteratorCompat
import org.thoughtcrime.securesms.R
@@ -68,6 +71,7 @@ class EditCallLinkNameDialogFragment : ComposeDialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val dialog = super.onCreateDialog(savedInstanceState)
dialog.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
dialog.window?.enableEdgeToEdge()
return dialog
}
@@ -143,7 +147,12 @@ private fun EditCallLinkNameScreen(
val focusRequester = remember { FocusRequester() }
val breakIterator = remember { BreakIteratorCompat.getInstance() }
Surface(modifier = Modifier.padding(paddingValues)) {
Surface(
modifier = Modifier
.padding(paddingValues)
.consumeWindowInsets(paddingValues)
.imePadding()
) {
Column(
modifier = Modifier
.padding(
@@ -91,28 +91,24 @@ class CallEventCache(
): CallLogRow.Call? {
val parent = next()
if (parent.type == Type.AD_HOC_CALL.code && !callLinksSeen.add(parent.peer)) {
return null
}
val isActiveCallLinkRow = parent.type != Type.AD_HOC_CALL.code || callLinksSeen.add(parent.peer)
val children = mutableSetOf<Long>()
while (hasNext()) {
val child = next()
if (child.type == Type.AD_HOC_CALL.code) {
previous()
break
}
if (parent.type != Type.AD_HOC_CALL.code) {
while (hasNext()) {
val child = next()
if (parent.peer == child.peer && parent.direction == child.direction && isEventMatch(parent, child) && isWithinTimeout(parent, child)) {
children.add(child.rowId)
} else {
previous()
break
if (parent.peer == child.peer && parent.direction == child.direction && isEventMatch(parent, child) && isWithinTimeout(parent, child)) {
children.add(child.rowId)
} else {
previous()
break
}
}
}
return createParentCallLogRow(parent, children, filterState, groupCallStateMap, canUserBeginCallMap)
return createParentCallLogRow(parent, children, filterState, groupCallStateMap, canUserBeginCallMap, isActiveCallLinkRow)
}
private fun readDataFromDatabase(): List<CacheRecord> {
@@ -170,7 +166,8 @@ class CallEventCache(
children: Set<Long>,
filterState: FilterState,
groupCallStateCache: MutableMap<Long, CallLogRow.GroupCallState>,
canUserBeginCallMap: MutableMap<Long, CallLogRow.CanStartCall>
canUserBeginCallMap: MutableMap<Long, CallLogRow.CanStartCall>,
isActiveCallLinkRow: Boolean
): CallLogRow.Call {
val peer = Recipient.resolved(RecipientId.from(parent.peer))
return CallLogRow.Call(
@@ -196,7 +193,7 @@ class CallEventCache(
},
children = setOf(parent.rowId) + children,
searchQuery = filterState.query,
callLinkPeekInfo = AppDependencies.signalCallManager.peekInfoSnapshot[peer.id],
callLinkPeekInfo = if (isActiveCallLinkRow) AppDependencies.signalCallManager.peekInfoSnapshot[peer.id] else null,
canUserBeginCall = if (peer.isGroup) {
canUserBeginCallMap.getOrPut(parent.peer) { canUserBeginCall(peer, parent.decryptedGroupBytes) }
} else {
@@ -10,7 +10,6 @@ import android.content.Intent
import android.os.Bundle
import androidx.activity.compose.LocalActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
@@ -60,7 +59,6 @@ class NewCallActivity : PassphraseRequiredActivity() {
}
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
enableEdgeToEdge()
super.onCreate(savedInstanceState, ready)
val navigateBack = onBackPressedDispatcher::onBackPressed
@@ -8,8 +8,6 @@ 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
@@ -124,8 +122,6 @@ private fun MessageDetailsEntry(route: MainNavigationDetailLocation.Chats.Messag
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.background)
.statusBarsPadding()
.navigationBarsPadding()
)
}
@@ -152,8 +148,6 @@ private fun ConversationSettingsEntry(route: MainNavigationDetailLocation.Chats.
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.background)
.statusBarsPadding()
.navigationBarsPadding()
) { fragment ->
backPressedState.attach(fragment)
}
@@ -0,0 +1,76 @@
/*
* Copyright 2026 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.clockskew
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.provider.Settings
import androidx.activity.addCallback
import androidx.activity.compose.setContent
import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import org.signal.core.ui.compose.theme.SignalTheme
import org.thoughtcrime.securesms.PassphraseRequiredActivity
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme
import org.thoughtcrime.securesms.util.viewModel
/**
* Hosts [ClockSkewScreen], the full-screen blocking screen shown when the local device clock is too far out of sync
* with the server's clock (see [ClockSkewDetector]). The user cannot leave until they fix their clock; the screen
* dismisses itself once the skew is resolved (which is re-checked when the app is backgrounded/foregrounded).
*/
class ClockSkewActivity : PassphraseRequiredActivity() {
private val theme = DynamicNoActionBarTheme()
private val viewModel: ClockSkewViewModel by viewModel { ClockSkewViewModel() }
override fun onPreCreate() {
theme.onCreate(this)
}
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
onBackPressedDispatcher.addCallback(this) {
// Disabled: the user must fix their clock.
}
lifecycleScope.launch {
viewModel.actions.collect { action ->
when (action) {
ClockSkewScreenAction.OpenDateSettings -> startActivity(Intent(Settings.ACTION_DATE_SETTINGS))
ClockSkewScreenAction.Finish -> finish()
}
}
}
setContent {
val state by viewModel.state.collectAsStateWithLifecycle()
SignalTheme {
ClockSkewScreen(
state = state,
onEvent = viewModel::onEvent
)
}
}
}
override fun onResume() {
super.onResume()
theme.onResume(this)
viewModel.onEvent(ClockSkewScreenEvent.ScreenResumed)
}
companion object {
@JvmStatic
fun createIntent(context: Context): Intent {
return Intent(context, ClockSkewActivity::class.java)
}
}
}
@@ -0,0 +1,137 @@
/*
* Copyright 2026 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.clockskew
import android.app.Application
import android.content.Intent
import android.os.SystemClock
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import org.signal.core.util.AppForegroundObserver
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.util.RemoteConfig
import kotlin.math.abs
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
/**
* Detects when the local device clock is too far out of sync with the server's clock and holds that state in memory.
*
* We learn the server's true time from two sources: the websocket ([org.signal.libsignal.net.ChatConnectionListener.onServerTimestamp],
* routed through [org.thoughtcrime.securesms.net.SignalWebSocketHealthMonitor]) and the remote config fetch. Because we
* hold the websocket open aggressively, a user can change their clock while already connected — in which case we won't
* receive a fresh server time. To catch that, we cache the last-known server time alongside a monotonic
* [SystemClock.elapsedRealtime] reading and re-check on foreground, estimating the current server time without needing
* another network round-trip.
*
* When skew is detected we stop trying to keep the websocket open (so we don't reconnect in a loop) regardless of
* whether the app is foregrounded, and — only while foregrounded — show [ClockSkewActivity]. The detection state is
* intentionally not persisted and is re-evaluated via [recheck] whenever the app is backgrounded or foregrounded, so a
* skew that has since been corrected clears itself and the user always gets a fresh attempt.
*
* Note that the monotonic reading is deliberately kept in memory only: it is meaningless across reboots, but a reboot
* kills our process anyway, so a live cache is always from the current boot.
*/
object ClockSkewDetector {
private val TAG = Log.tag(ClockSkewDetector::class)
private val _detected = MutableStateFlow(false)
val detected: StateFlow<Boolean> = _detected.asStateFlow()
val isDetected: Boolean
get() = _detected.value
/** The amount our local clock was off from the server's when skew was detected. [Duration.ZERO] when not detected. */
@Volatile
var skew: Duration = Duration.ZERO
private set
@Volatile
private var lastServerTime: Long = 0
@Volatile
private var lastServerTimeElapsedRealtime: Long = 0
private val allowedSkew: Duration
get() = RemoteConfig.maxAllowedClockSkewSeconds.seconds
/**
* Records a freshly-observed server time (both persisting it and caching it for [recheck]) and immediately checks it
* against our local clock.
*/
fun onServerTimeReceived(serverTime: Long) {
lastServerTime = serverTime
lastServerTimeElapsedRealtime = SystemClock.elapsedRealtime()
SignalStore.misc.setLastKnownServerTime(serverTime, System.currentTimeMillis())
val skew = skewFrom(serverTime)
if (skew > allowedSkew) {
Log.w(TAG, "Local clock is off from the server by $skew, which exceeds the allowed limit. Blocking.", true)
markDetected(skew)
}
}
/**
* Re-evaluates clock skew using our cached server time and a monotonic estimate of elapsed time, without needing a
* network round-trip. Intended to be called when the app is foregrounded, to catch the case where the user changed
* their clock while we were already connected. Clears the detection state if the clock now looks fine.
*/
fun recheck() {
if (lastServerTimeElapsedRealtime == 0L) {
reset()
return
}
val estimatedServerTime = lastServerTime + (SystemClock.elapsedRealtime() - lastServerTimeElapsedRealtime)
val skew = skewFrom(estimatedServerTime)
if (skew > allowedSkew) {
Log.w(TAG, "Local clock is off from the estimated server time by $skew, which exceeds the allowed limit. Blocking.", true)
markDetected(skew)
} else {
reset()
}
}
/** Clears any detected skew, allowing the websocket to reconnect and re-check. */
private fun reset() {
skew = Duration.ZERO
_detected.value = false
}
/**
* Begins observing the detection state and launches [ClockSkewActivity] whenever clock skew is detected while the app
* is foregrounded. Skew can also be detected while backgrounded (which still blocks the websocket), but we only bring
* up the blocking screen when foregrounded, to avoid a background activity launch. Should be called once during app
* startup.
*/
@JvmStatic
fun beginObserving(application: Application) {
CoroutineScope(Dispatchers.Main).launch {
detected.collect { detected ->
if (detected && AppForegroundObserver.isForegrounded()) {
application.startActivity(ClockSkewActivity.createIntent(application).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
}
}
}
}
private fun markDetected(skew: Duration) {
this.skew = skew
_detected.value = true
}
private fun skewFrom(serverTime: Long): Duration {
return abs(System.currentTimeMillis() - serverTime).milliseconds
}
}
@@ -0,0 +1,135 @@
/*
* Copyright 2026 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.clockskew
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.displayCutoutPadding
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import org.signal.core.ui.compose.AllDevicePreviews
import org.signal.core.ui.compose.Buttons
import org.signal.core.ui.compose.Previews
import org.thoughtcrime.securesms.R
/**
* Full-screen blocking screen shown when we've detected that the local device clock is too far out of sync with the
* server's clock (see [ClockSkewDetector]).
*/
@Composable
fun ClockSkewScreen(
state: ClockSkewState,
onEvent: (ClockSkewScreenEvent) -> Unit,
modifier: Modifier = Modifier
) {
Surface(modifier = modifier) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.fillMaxSize()
.systemBarsPadding()
.displayCutoutPadding()
.padding(horizontal = 32.dp, vertical = 24.dp)
) {
Text(
text = stringResource(R.string.ClockSkewActivity__title),
style = MaterialTheme.typography.headlineLarge,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
BoxWithConstraints(
modifier = Modifier
.weight(1f)
.fillMaxWidth()
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = Modifier
.fillMaxWidth()
.heightIn(min = maxHeight)
.verticalScroll(rememberScrollState())
) {
Spacer(modifier = Modifier.height(64.dp))
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.size(150.dp)
.background(color = MaterialTheme.colorScheme.surfaceVariant, shape = CircleShape)
) {
Icon(
painter = painterResource(R.drawable.symbol_recent_24),
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(96.dp)
)
}
Spacer(modifier = Modifier.height(64.dp))
Text(
text = stringResource(R.string.ClockSkewActivity__date_inaccurate_description),
style = MaterialTheme.typography.bodyLarge,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = stringResource(R.string.ClockSkewActivity__time_prefix, state.deviceDateTime),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
}
}
Buttons.LargePrimary(
onClick = { onEvent(ClockSkewScreenEvent.AdjustDateSelected) },
modifier = Modifier.fillMaxWidth()
) {
Text(text = stringResource(R.string.ClockSkewActivity__adjust_date_button))
}
}
}
}
@AllDevicePreviews
@Composable
private fun ClockSkewScreenPreview() {
Previews.Preview {
ClockSkewScreen(
state = ClockSkewState(deviceDateTime = "12/17/25, 11:26 PM\n(Greenwich Mean Time)"),
onEvent = {}
)
}
}
@@ -0,0 +1,18 @@
/*
* Copyright 2026 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.clockskew
/**
* One-shot side effects emitted by [ClockSkewViewModel] for the host [ClockSkewActivity] to perform, since they require
* an Activity context.
*/
sealed interface ClockSkewScreenAction {
/** Open the system date and time settings. */
data object OpenDateSettings : ClockSkewScreenAction
/** Skew has been resolved; the blocking screen should close. */
data object Finish : ClockSkewScreenAction
}
@@ -0,0 +1,20 @@
/*
* Copyright 2026 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.clockskew
/**
* Events emitted by [ClockSkewScreen] and handled by [ClockSkewViewModel].
*/
sealed interface ClockSkewScreenEvent {
/** The screen became visible; the displayed device time should be recomputed. */
data object ScreenResumed : ClockSkewScreenEvent
/** The user tapped the button to adjust their device date. */
data object AdjustDateSelected : ClockSkewScreenEvent
/** Internal: the [ClockSkewDetector]'s detection state changed. */
data class SkewStateChanged(val detected: Boolean) : ClockSkewScreenEvent
}
@@ -0,0 +1,14 @@
/*
* Copyright 2026 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.clockskew
/**
* View state for [ClockSkewScreen].
*/
data class ClockSkewState(
/** The current device date and time, formatted for display (including the time zone). */
val deviceDateTime: String = ""
)
@@ -0,0 +1,68 @@
/*
* Copyright 2026 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.clockskew
import androidx.annotation.VisibleForTesting
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.receiveAsFlow
import org.signal.core.ui.compose.EventDrivenViewModel
import org.signal.core.util.logging.Log
import java.text.DateFormat
import java.util.Date
import java.util.TimeZone
class ClockSkewViewModel(
private val clock: () -> Long = { System.currentTimeMillis() },
detected: StateFlow<Boolean> = ClockSkewDetector.detected
) : EventDrivenViewModel<ClockSkewScreenEvent>(TAG) {
companion object {
private val TAG = Log.tag(ClockSkewViewModel::class)
}
private val _state = MutableStateFlow(ClockSkewState(deviceDateTime = formatDeviceDateTime()))
val state: StateFlow<ClockSkewState> = _state.asStateFlow()
private val _actions = Channel<ClockSkewScreenAction>(Channel.UNLIMITED)
val actions: Flow<ClockSkewScreenAction> = _actions.receiveAsFlow()
init {
detected
.onEach { onEvent(ClockSkewScreenEvent.SkewStateChanged(it)) }
.launchIn(viewModelScope)
}
override suspend fun processEvent(event: ClockSkewScreenEvent) {
applyEvent(_state.value, event, { _actions.trySend(it) }) { _state.value = it }
}
@VisibleForTesting
fun applyEvent(
state: ClockSkewState,
event: ClockSkewScreenEvent,
actionEmitter: (ClockSkewScreenAction) -> Unit,
stateEmitter: (ClockSkewState) -> Unit
) {
when (event) {
ClockSkewScreenEvent.ScreenResumed -> stateEmitter(state.copy(deviceDateTime = formatDeviceDateTime()))
ClockSkewScreenEvent.AdjustDateSelected -> actionEmitter(ClockSkewScreenAction.OpenDateSettings)
is ClockSkewScreenEvent.SkewStateChanged -> if (!event.detected) actionEmitter(ClockSkewScreenAction.Finish)
}
}
private fun formatDeviceDateTime(): String {
val dateTime = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT).format(Date(clock()))
val timeZone = TimeZone.getDefault().getDisplayName(false, TimeZone.LONG)
return "$dateTime\n($timeZone)"
}
}
@@ -1,5 +1,6 @@
package org.thoughtcrime.securesms.components;
import android.app.Dialog;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
@@ -12,7 +13,9 @@ import androidx.annotation.StringRes;
import androidx.appcompat.widget.Toolbar;
import androidx.fragment.app.DialogFragment;
import org.signal.core.ui.WindowExtensionsKt;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.SystemWindowInsetsSetter;
import org.thoughtcrime.securesms.util.WindowUtil;
/**
@@ -30,6 +33,15 @@ public abstract class FullScreenDialogFragment extends DialogFragment {
setStyle(STYLE_NO_FRAME, R.style.Signal_DayNight_Dialog_FullScreen);
}
@Override
public @NonNull Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
Dialog dialog = super.onCreateDialog(savedInstanceState);
if (dialog.getWindow() != null) {
WindowExtensionsKt.enableEdgeToEdge(dialog.getWindow());
}
return dialog;
}
@Override
public final @NonNull View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.full_screen_dialog_fragment, container, false);
@@ -41,6 +53,8 @@ public abstract class FullScreenDialogFragment extends DialogFragment {
}
toolbar.setNavigationOnClickListener(v -> onNavigateUp());
SystemWindowInsetsSetter.attach(view, getViewLifecycleOwner());
return view;
}
@@ -17,40 +17,37 @@
package org.thoughtcrime.securesms.components;
import android.content.Context;
import android.graphics.Rect;
import android.os.Build;
import android.content.res.Configuration;
import android.preference.PreferenceManager;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.view.Surface;
import android.view.View;
import android.view.WindowInsets;
import androidx.annotation.NonNull;
import androidx.appcompat.widget.LinearLayoutCompat;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.R;
import org.signal.core.util.ServiceUtil;
import org.signal.core.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
import java.lang.reflect.Field;
import java.util.HashSet;
import java.util.Set;
/**
* LinearLayout that, when a view container, will report back when it thinks a soft keyboard
* has been opened and what its height would be.
* has been opened and what its height would be. Driven by {@link WindowInsetsCompat.Type#ime()}
* insets, so it requires an edge-to-edge host window (all activity and dialog windows in the app
* are edge-to-edge). The reported height excludes the navigation bar, matching what a custom
* keyboard sitting above a navigation-bar-padded container should be sized to.
*/
public class KeyboardAwareLinearLayout extends LinearLayoutCompat {
private static final String TAG = Log.tag(KeyboardAwareLinearLayout.class);
private static final long KEYBOARD_DEBOUNCE = 150;
private final Rect rect = new Rect();
private final Set<OnKeyboardHiddenListener> hiddenListeners = new HashSet<>();
private final Set<OnKeyboardShownListener> shownListeners = new HashSet<>();
private final DisplayMetrics displayMetrics = new DisplayMetrics();
private final int minKeyboardSize;
private final int minCustomKeyboardSize;
@@ -58,14 +55,14 @@ public class KeyboardAwareLinearLayout extends LinearLayoutCompat {
private final int minCustomKeyboardTopMarginPortrait;
private final int minCustomKeyboardTopMarginLandscape;
private final int minCustomKeyboardTopMarginLandscapeBubble;
private final int statusBarHeight;
private int viewInset;
private boolean keyboardOpen = false;
private int rotation = 0;
private boolean isBubble = false;
private long openedAt = 0;
private int lastKeyboardHeight;
private boolean lastKeyboardVisible;
private InsetPaddingMode insetPaddingMode = InsetPaddingMode.NONE;
public KeyboardAwareLinearLayout(Context context) {
this(context, null);
@@ -83,48 +80,71 @@ public class KeyboardAwareLinearLayout extends LinearLayoutCompat {
minCustomKeyboardTopMarginPortrait = getResources().getDimensionPixelSize(R.dimen.min_custom_keyboard_top_margin_portrait);
minCustomKeyboardTopMarginLandscape = getResources().getDimensionPixelSize(R.dimen.min_custom_keyboard_top_margin_portrait);
minCustomKeyboardTopMarginLandscapeBubble = getResources().getDimensionPixelSize(R.dimen.min_custom_keyboard_top_margin_landscape_bubble);
statusBarHeight = ViewUtil.getStatusBarHeight(this);
viewInset = getViewInset();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
updateRotation();
updateKeyboardState();
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
ViewCompat.setOnApplyWindowInsetsListener(this, (view, insets) -> {
Insets ime = insets.getInsets(WindowInsetsCompat.Type.ime());
Insets nav = insets.getInsets(WindowInsetsCompat.Type.navigationBars());
Insets bars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
lastKeyboardHeight = Math.max(0, ime.bottom - nav.bottom);
lastKeyboardVisible = insets.isVisible(WindowInsetsCompat.Type.ime());
switch (insetPaddingMode) {
case NONE:
break;
case KEYBOARD:
setPadding(getPaddingLeft(), getPaddingTop(), getPaddingRight(), lastKeyboardVisible ? lastKeyboardHeight : 0);
break;
case KEYBOARD_AND_NAVIGATION_BAR:
setPadding(getPaddingLeft(), getPaddingTop(), getPaddingRight(), Math.max(nav.bottom, ime.bottom));
break;
case SAFE_AREA:
setPadding(bars.left, bars.top, bars.right, Math.max(bars.bottom, ime.bottom));
break;
}
updateKeyboardState();
return insets;
});
}
public void setIsBubble(boolean isBubble) {
this.isBubble = isBubble;
}
private void updateRotation() {
int oldRotation = rotation;
rotation = getDeviceRotation();
if (oldRotation != rotation) {
Log.i(TAG, "rotation changed");
onKeyboardClose();
}
/**
* Opt-in self-padding, replacing the window resize behavior these layouts relied on before the
* host windows went edge-to-edge. [InsetPaddingMode.NONE] keeps the layout as a pure keyboard
* event source (e.g. when a parent container already manages insets).
*/
public void setInsetPaddingMode(@NonNull InsetPaddingMode mode) {
this.insetPaddingMode = mode;
ViewCompat.requestApplyInsets(this);
}
public enum InsetPaddingMode {
/** Apply no padding; the layout only reports keyboard events. */
NONE,
/** Pad the bottom by the keyboard height above the navigation bar (use inside a container that already clears the navigation bar). */
KEYBOARD,
/** Pad the bottom by the keyboard or navigation bar, whichever is larger. */
KEYBOARD_AND_NAVIGATION_BAR,
/** Pad all sides by the system bars, and the bottom by the keyboard when it is taller. */
SAFE_AREA
}
private void updateKeyboardState() {
if (viewInset == 0) viewInset = getViewInset();
getWindowVisibleDisplayFrame(rect);
final int availableHeight = getAvailableHeight();
final int keyboardHeight = availableHeight - rect.bottom;
if (keyboardHeight > minKeyboardSize) {
if (getKeyboardHeight() != keyboardHeight) {
if (lastKeyboardVisible && lastKeyboardHeight > minKeyboardSize) {
if (getKeyboardHeight() != lastKeyboardHeight) {
if (isLandscape()) {
setKeyboardLandscapeHeight(keyboardHeight);
setKeyboardLandscapeHeight(lastKeyboardHeight);
} else {
setKeyboardPortraitHeight(keyboardHeight);
setKeyboardPortraitHeight(lastKeyboardHeight);
}
}
if (!keyboardOpen) {
onKeyboardOpen(keyboardHeight);
onKeyboardOpen(lastKeyboardHeight);
}
} else if (keyboardOpen) {
onKeyboardClose();
@@ -134,53 +154,7 @@ public class KeyboardAwareLinearLayout extends LinearLayoutCompat {
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
rotation = getDeviceRotation();
if (getRootWindowInsets() != null) {
int bottomInset;
WindowInsets windowInsets = getRootWindowInsets();
if (Build.VERSION.SDK_INT >= 30) {
bottomInset = windowInsets.getInsets(WindowInsets.Type.navigationBars()).bottom;
} else {
bottomInset = windowInsets.getStableInsetBottom();
}
if (bottomInset != 0 && (viewInset == 0 || viewInset == statusBarHeight)) {
Log.i(TAG, "Updating view inset based on WindowInsets. viewInset: " + viewInset + " windowInset: " + bottomInset);
viewInset = bottomInset;
}
}
}
private int getViewInset() {
try {
Field attachInfoField = View.class.getDeclaredField("mAttachInfo");
attachInfoField.setAccessible(true);
Object attachInfo = attachInfoField.get(this);
if (attachInfo != null) {
Field stableInsetsField = attachInfo.getClass().getDeclaredField("mStableInsets");
stableInsetsField.setAccessible(true);
Rect insets = (Rect) stableInsetsField.get(attachInfo);
if (insets != null) {
return insets.bottom;
}
}
} catch (NoSuchFieldException | IllegalAccessException e) {
// Do nothing
}
return statusBarHeight;
}
private int getAvailableHeight() {
final int availableHeight = this.getRootView().getHeight() - viewInset;
final int availableWidth = this.getRootView().getWidth();
if (isLandscape() && availableHeight > availableWidth) {
//noinspection SuspiciousNameCombination
return availableWidth;
}
return availableHeight;
ViewCompat.requestApplyInsets(this);
}
protected void onKeyboardOpen(int keyboardHeight) {
@@ -213,21 +187,7 @@ public class KeyboardAwareLinearLayout extends LinearLayoutCompat {
}
public boolean isLandscape() {
int rotation = getDeviceRotation();
return rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270;
}
private int getDeviceRotation() {
if (isInEditMode()) {
return Surface.ROTATION_0;
}
if (Build.VERSION.SDK_INT >= 30) {
getContext().getDisplay().getRealMetrics(displayMetrics);
} else {
ServiceUtil.getWindowManager(getContext()).getDefaultDisplay().getRealMetrics(displayMetrics);
}
return displayMetrics.widthPixels > displayMetrics.heightPixels ? Surface.ROTATION_90 : Surface.ROTATION_0;
return getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE;
}
private int getKeyboardLandscapeHeight() {
@@ -8,6 +8,7 @@ import android.view.ViewGroup
import android.view.WindowManager
import androidx.annotation.LayoutRes
import androidx.fragment.app.DialogFragment
import org.signal.core.ui.enableEdgeToEdge
import org.thoughtcrime.securesms.R
/**
@@ -38,6 +39,7 @@ abstract class KeyboardEntryDialogFragment(@LayoutRes contentLayoutId: Int) :
}
dialog.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE or WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE)
dialog.window?.enableEdgeToEdge()
return dialog
}
@@ -47,6 +49,7 @@ abstract class KeyboardEntryDialogFragment(@LayoutRes contentLayoutId: Int) :
val view = super.onCreateView(inflater, container, savedInstanceState)
return if (view is KeyboardAwareLinearLayout) {
view.setInsetPaddingMode(KeyboardAwareLinearLayout.InsetPaddingMode.SAFE_AREA)
view.addOnKeyboardShownListener(this)
view.addOnKeyboardHiddenListener(this)
view
@@ -13,6 +13,7 @@ import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.constraintlayout.widget.ConstraintLayout;
import com.bumptech.glide.RequestManager;
@@ -207,6 +208,27 @@ public class LinkPreviewView extends FrameLayout {
} else {
thumbnail.setVisibility(GONE);
}
boolean thumbnailVisible = (showThumbnail && linkPreview.getThumbnail().isPresent()) || callLinkRootKey != null;
alignTitleWithThumbnail(thumbnailVisible);
}
private void alignTitleWithThumbnail(boolean thumbnailVisible) {
ConstraintLayout.LayoutParams params = (ConstraintLayout.LayoutParams) title.getLayoutParams();
if (thumbnailVisible) {
params.topToTop = R.id.linkpreview_thumbnail;
params.bottomToBottom = R.id.linkpreview_thumbnail;
params.startToEnd = R.id.linkpreview_thumbnail;
params.startToStart = ConstraintLayout.LayoutParams.UNSET;
params.setMarginStart(ViewUtil.dpToPx(8));
} else {
params.topToTop = ConstraintLayout.LayoutParams.PARENT_ID;
params.bottomToBottom = ConstraintLayout.LayoutParams.UNSET;
params.startToEnd = ConstraintLayout.LayoutParams.UNSET;
params.startToStart = ConstraintLayout.LayoutParams.PARENT_ID;
params.setMarginStart(0);
}
title.setLayoutParams(params);
}
public void setCorners(int topStart, int topEnd) {
@@ -22,6 +22,7 @@ import com.google.android.material.imageview.ShapeableImageView;
import com.google.android.material.shape.CornerFamily;
import com.google.android.material.shape.ShapeAppearanceModel;
import org.signal.core.ui.fonts.SignalSymbols;
import org.signal.core.ui.view.Stub;
import org.signal.core.util.DimensionUnit;
import org.signal.core.util.Util;
@@ -37,7 +38,6 @@ import org.thoughtcrime.securesms.conversation.MessageStyler;
import org.thoughtcrime.securesms.conversation.v2.items.SenderNameWithLabelView;
import org.thoughtcrime.securesms.database.model.Mention;
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList;
import org.thoughtcrime.securesms.fonts.SignalSymbols;
import org.thoughtcrime.securesms.groups.memberlabel.MemberLabel;
import org.thoughtcrime.securesms.mms.QuoteModel;
import org.thoughtcrime.securesms.mms.Slide;
@@ -1,11 +1,13 @@
package org.thoughtcrime.securesms.components
import android.app.Dialog
import android.content.DialogInterface
import android.os.Bundle
import android.view.View
import androidx.activity.OnBackPressedCallback
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment
import org.signal.core.ui.enableEdgeToEdge
import org.signal.core.ui.initializeScreenshotSecurity
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.util.fragments.findListener
@@ -41,6 +43,12 @@ abstract class WrapperDialogFragment : DialogFragment(R.layout.fragment_containe
setStyle(STYLE_NO_FRAME, R.style.Signal_DayNight_Dialog_FullScreen)
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return super.onCreateDialog(savedInstanceState).apply {
window?.enableEdgeToEdge()
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
requireActivity().onBackPressedDispatcher.addCallback(
viewLifecycleOwner,
@@ -17,7 +17,7 @@ import java.util.Set;
import java.util.regex.Pattern;
public final class EmojiUtil {
private static final Pattern EMOJI_PATTERN = Pattern.compile("^(?:(?:[\u00a9\u00ae\u203c\u2049\u2122\u2139\u2194-\u2199\u21a9-\u21aa\u231a-\u231b\u2328\u23cf\u23e9-\u23f3\u23f8-\u23fa\u24c2\u25aa-\u25ab\u25b6\u25c0\u25fb-\u25fe\u2600-\u2604\u260e\u2611\u2614-\u2615\u2618\u261d\u2620\u2622-\u2623\u2626\u262a\u262e-\u262f\u2638-\u263a\u2648-\u2653\u2660\u2663\u2665-\u2666\u2668\u267b\u267f\u2692-\u2694\u2696-\u2697\u2699\u269b-\u269c\u26a0-\u26a1\u26aa-\u26ab\u26b0-\u26b1\u26bd-\u26be\u26c4-\u26c5\u26c8\u26ce-\u26cf\u26d1\u26d3-\u26d4\u26e9-\u26ea\u26f0-\u26f5\u26f7-\u26fa\u26fd\u2702\u2705\u2708-\u270d\u270f\u2712\u2714\u2716\u271d\u2721\u2728\u2733-\u2734\u2744\u2747\u274c\u274e\u2753-\u2755\u2757\u2763-\u2764\u2795-\u2797\u27a1\u27b0\u27bf\u2934-\u2935\u2b05-\u2b07\u2b1b-\u2b1c\u2b50\u2b55\u3030\u303d\u3297\u3299\ud83c\udc04\ud83c\udccf\ud83c\udd70-\ud83c\udd71\ud83c\udd7e-\ud83c\udd7f\ud83c\udd8e\ud83c\udd91-\ud83c\udd9a\ud83c\ude01-\ud83c\ude02\ud83c\ude1a\ud83c\ude2f\ud83c\ude32-\ud83c\ude3a\ud83c\ude50-\ud83c\ude51\u200d\ud83c\udf00-\ud83d\uddff\ud83d\ude00-\ud83d\ude4f\ud83d\ude80-\ud83d\udeff\ud83e\udd00-\ud83e\uddff\udb40\udc20-\udb40\udc7f]|\u200d[\u2640\u2642]|[\ud83c\udde6-\ud83c\uddff]{2}|.[\u20e0\u20e3\ufe0f]+)+)+$");
private static final Pattern EMOJI_PATTERN = Pattern.compile("^(?:[\u00a9\u00ae\u203c\u2049\u2122\u2139\u2194-\u2199\u21a9-\u21aa\u231a-\u231b\u2328\u23cf\u23e9-\u23f3\u23f8-\u23fa\u24c2\u25aa-\u25ab\u25b6\u25c0\u25fb-\u25fe\u2600-\u2604\u260e\u2611\u2614-\u2615\u2618\u261d\u2620\u2622-\u2623\u2626\u262a\u262e-\u262f\u2638-\u263a\u2648-\u2653\u2660\u2663\u2665-\u2666\u2668\u267b\u267f\u2692-\u2694\u2696-\u2697\u2699\u269b-\u269c\u26a0-\u26a1\u26aa-\u26ab\u26b0-\u26b1\u26bd-\u26be\u26c4-\u26c5\u26c8\u26ce-\u26cf\u26d1\u26d3-\u26d4\u26e9-\u26ea\u26f0-\u26f5\u26f7-\u26fa\u26fd\u2702\u2705\u2708-\u270d\u270f\u2712\u2714\u2716\u271d\u2721\u2728\u2733-\u2734\u2744\u2747\u274c\u274e\u2753-\u2755\u2757\u2763-\u2764\u2795-\u2797\u27a1\u27b0\u27bf\u2934-\u2935\u2b05-\u2b07\u2b1b-\u2b1c\u2b50\u2b55\u3030\u303d\u3297\u3299\ud83c\udc04\ud83c\udccf\ud83c\udd70-\ud83c\udd71\ud83c\udd7e-\ud83c\udd7f\ud83c\udd8e\ud83c\udd91-\ud83c\udd9a\ud83c\ude01-\ud83c\ude02\ud83c\ude1a\ud83c\ude2f\ud83c\ude32-\ud83c\ude3a\ud83c\ude50-\ud83c\ude51\u200d\ud83c\udf00-\ud83d\uddff\ud83d\ude00-\ud83d\ude4f\ud83d\ude80-\ud83d\udeff\ud83e\udd00-\ud83e\uddff\udb40\udc20-\udb40\udc7f]|\u200d[\u2640\u2642]|[\ud83c\udde6-\ud83c\uddff]{2}|.[\u20e0\u20e3\ufe0f]++)+$");
private static final String EMOJI_REGEX = "[^\\p{L}\\p{M}\\p{N}\\p{P}\\p{Z}\\p{Cf}\\p{Cs}\\s]";
private EmojiUtil() {}
@@ -1,7 +1,12 @@
package org.thoughtcrime.securesms.components.settings
import android.content.res.Configuration
import android.os.Build
import android.os.Bundle
import androidx.activity.OnBackPressedCallback
import androidx.activity.SystemBarStyle
import androidx.activity.enableEdgeToEdge
import androidx.core.content.ContextCompat
import androidx.navigation.NavController
import androidx.navigation.Navigation
import androidx.navigation.fragment.NavHostFragment
@@ -9,6 +14,7 @@ import org.thoughtcrime.securesms.PassphraseRequiredActivity
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme
import org.thoughtcrime.securesms.util.DynamicTheme
import org.signal.core.ui.R as CoreUiR
/**
* The DSL API can be completely replaced by compose.
@@ -22,6 +28,8 @@ open class DSLSettingsActivity : PassphraseRequiredActivity() {
private set
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
enableSettingsEdgeToEdge()
setContentView(R.layout.dsl_settings_activity)
if (savedInstanceState == null) {
@@ -62,6 +70,23 @@ open class DSLSettingsActivity : PassphraseRequiredActivity() {
}
}
private fun enableSettingsEdgeToEdge() {
val navBarColor = ContextCompat.getColor(this, CoreUiR.color.signal_colorSurface2)
val isDark = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
if (Build.VERSION.SDK_INT >= 26) {
enableEdgeToEdge(
navigationBarStyle = if (isDark) {
SystemBarStyle.dark(navBarColor)
} else {
SystemBarStyle.light(navBarColor, navBarColor)
}
)
} else {
enableEdgeToEdge()
}
}
protected open fun onWillFinish() {}
protected open fun resolveNavGraphId(): Int = intent.getIntExtra(ARG_NAV_GRAPH, -1)
@@ -4,6 +4,7 @@ import android.content.Context
import android.os.Build
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import android.widget.EdgeEffect
import androidx.annotation.CallSuper
import androidx.annotation.LayoutRes
@@ -11,12 +12,17 @@ import androidx.annotation.MenuRes
import androidx.annotation.StringRes
import androidx.appcompat.widget.Toolbar
import androidx.core.content.ContextCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updateLayoutParams
import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import org.signal.core.ui.logging.LoggingFragment
import org.thoughtcrime.securesms.MainActivity
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.conversation.ConversationSettingsNavHostFragment
import org.thoughtcrime.securesms.util.Material3OnScrollHelper
import org.thoughtcrime.securesms.util.SystemWindowInsetsSetter
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
/**
@@ -70,6 +76,8 @@ abstract class DSLSettingsFragment(
}
}
applyEdgeToEdgeInsets()
when (settingsAdapter) {
is ConcatAdapter -> bindAdapters(settingsAdapter)
is MappingAdapter -> bindAdapter(settingsAdapter)
@@ -77,6 +85,39 @@ abstract class DSLSettingsFragment(
}
}
/**
* All host windows are edge-to-edge activities centrally via [org.thoughtcrime.securesms.BaseActivity],
* and the full-screen dialog hosts ([org.thoughtcrime.securesms.components.WrapperDialogFragment],
* [org.thoughtcrime.securesms.stories.settings.create.CreateStoryFlowDialogFragment]) via their own
* `enableEdgeToEdge` calls so the toolbar tint must extend behind the (transparent) status bar and the
* list must scroll clear of the navigation bar. Skipped only for fragments embedded directly in
* [MainActivity]'s Compose scaffolding, which insets those containers itself except the
* conversation-settings detail entry ([ConversationSettingsNavHostFragment]), which leaves insets to these
* fragments so the toolbar scroll tint can reach behind the status bar.
*/
private fun applyEdgeToEdgeInsets() {
if (isInsetByMainActivityHost()) {
return
}
toolbar?.let { toolbar ->
toolbar.updateLayoutParams { height = ViewGroup.LayoutParams.WRAP_CONTENT }
SystemWindowInsetsSetter.attach(toolbar, viewLifecycleOwner, WindowInsetsCompat.Type.statusBars())
}
recyclerView?.let { recycler ->
recycler.clipToPadding = false
SystemWindowInsetsSetter.attach(recycler, viewLifecycleOwner, WindowInsetsCompat.Type.navigationBars())
}
}
private fun isInsetByMainActivityHost(): Boolean {
if (activity !is MainActivity) {
return false
}
return generateSequence(parentFragment) { it.parentFragment }.none { it is ConversationSettingsNavHostFragment }
}
open fun getMaterial3OnScrollHelper(toolbar: Toolbar?): Material3OnScrollHelper? {
if (toolbar == null) {
return null
@@ -74,7 +74,6 @@ import org.thoughtcrime.securesms.components.settings.app.routes.AppSettingsRout
import org.thoughtcrime.securesms.components.settings.app.subscription.BadgeImageMedium
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
import org.thoughtcrime.securesms.components.settings.app.subscription.completed.InAppPaymentsBottomSheetDelegate
import org.thoughtcrime.securesms.compose.rememberStatusBarColorNestedScrollModifier
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.profiles.ProfileName
@@ -219,9 +218,7 @@ private fun AppSettingsContent(
) {
bannerManager.Banner()
LazyColumn(
modifier = rememberStatusBarColorNestedScrollModifier()
) {
LazyColumn {
item {
BioRow(
self = self,
@@ -47,7 +47,6 @@ import org.signal.core.ui.compose.Scaffolds
import org.signal.core.ui.compose.Texts
import org.signal.core.util.ServiceUtil
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.compose.rememberStatusBarColorNestedScrollModifier
import org.thoughtcrime.securesms.contactshare.SimpleTextWatcher
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.keyvalue.SignalStore
@@ -265,9 +264,7 @@ fun AccountSettingsScreen(
) { contentPadding ->
LazyColumn(
modifier = Modifier
.padding(contentPadding)
.then(rememberStatusBarColorNestedScrollModifier())
.testTag(AccountSettingsTestTags.SCROLLER)
.padding(contentPadding).testTag(AccountSettingsTestTags.SCROLLER)
) {
item {
Texts.SectionHeader(
@@ -41,7 +41,6 @@ import org.signal.core.ui.compose.theme.SignalTheme
import org.signal.core.util.ServiceUtil
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.app.account.LinkedDeviceAccountSettingsState.OneTimeEvent
import org.thoughtcrime.securesms.compose.rememberStatusBarColorNestedScrollModifier
import org.thoughtcrime.securesms.dependencies.AppDependencies
/**
@@ -92,7 +91,6 @@ private fun LinkedDeviceAccountSettingsScreen(
LazyColumn(
modifier = Modifier
.padding(paddingValues)
.then(rememberStatusBarColorNestedScrollModifier())
) {
item {
LinkedDeviceCallout(onLearnMoreClick = { onEvent(LinkedDeviceAccountSettingsEvent.LearnMoreClicked) })
@@ -24,7 +24,6 @@ import org.signal.core.ui.compose.Scaffolds
import org.signal.core.ui.compose.SignalIcons
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.app.appearance.navbar.ChooseNavigationBarStyleFragment
import org.thoughtcrime.securesms.compose.rememberStatusBarColorNestedScrollModifier
import org.thoughtcrime.securesms.keyvalue.SettingsValues
import org.thoughtcrime.securesms.util.navigation.safeNavigate
@@ -114,7 +113,6 @@ private fun AppearanceSettingsScreen(
LazyColumn(
modifier = Modifier
.padding(paddingValues)
.then(rememberStatusBarColorNestedScrollModifier())
) {
item {
Rows.RadioListRow(
@@ -106,7 +106,6 @@ import org.thoughtcrime.securesms.components.compose.rememberBiometricsAuthentic
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity
import org.thoughtcrime.securesms.components.settings.app.backups.BackupState
import org.thoughtcrime.securesms.components.settings.app.subscription.MessageBackupsCheckoutLauncher.createBackupsCheckoutLauncher
import org.thoughtcrime.securesms.compose.StatusBarColorNestedScrollConnection
import org.thoughtcrime.securesms.help.HelpFragment
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.keyvalue.protos.ArchiveUploadProgressState
@@ -151,8 +150,7 @@ class RemoteBackupsSettingsFragment : ComposeFragment() {
backupRestoreState = restoreState,
backupDeleteState = deleteState,
contentCallbacks = callbacks,
backupProgress = backupProgress,
statusBarColorNestedScrollConnection = remember { StatusBarColorNestedScrollConnection(requireActivity()) }
backupProgress = backupProgress
)
}
@@ -360,8 +358,7 @@ private fun RemoteBackupsSettingsContent(
backupRestoreState: BackupRestoreState,
backupDeleteState: DeletionState,
contentCallbacks: ContentCallbacks,
backupProgress: ArchiveUploadProgressState?,
statusBarColorNestedScrollConnection: StatusBarColorNestedScrollConnection?
backupProgress: ArchiveUploadProgressState?
) {
val context = LocalContext.current
val biometrics = rememberBiometricsAuthentication(
@@ -402,13 +399,6 @@ private fun RemoteBackupsSettingsContent(
)
},
modifier = Modifier
.then(
if (statusBarColorNestedScrollConnection != null) {
Modifier.nestedScroll(statusBarColorNestedScrollConnection)
} else {
Modifier
}
)
.nestedScroll(scrollBehavior.nestedScrollConnection),
snackbarHost = {
Snackbars.Host(snackbarHostState = snackbarHostState)
@@ -1885,7 +1875,6 @@ private fun RemoteBackupsSettingsContentPreview() {
hasRedemptionError = true,
isOutOfStorageSpace = true
),
statusBarColorNestedScrollConnection = null,
backupDeleteState = DeletionState.NONE,
backupRestoreState = BackupRestoreState.None,
contentCallbacks = ContentCallbacks.Empty,
@@ -1915,7 +1904,6 @@ private fun RemoteBackupsSettingsInternalUserContentPreview() {
internalUser = true,
includeDebuglog = true
),
statusBarColorNestedScrollConnection = null,
backupDeleteState = DeletionState.NONE,
backupRestoreState = BackupRestoreState.None,
contentCallbacks = ContentCallbacks.Empty,
@@ -1948,7 +1936,6 @@ private fun RemoteBackupsSettingsLinkedDevicePaidContentPreview() {
renewalTime = 1_752_710_400.seconds
)
),
statusBarColorNestedScrollConnection = null,
backupDeleteState = DeletionState.NONE,
backupRestoreState = BackupRestoreState.None,
contentCallbacks = ContentCallbacks.Empty,
@@ -1973,7 +1960,6 @@ private fun RemoteBackupsSettingsLinkedDeviceNotFoundContentPreview() {
backupMediaSize = 2300000,
backupState = BackupState.NotFound
),
statusBarColorNestedScrollConnection = null,
backupDeleteState = DeletionState.NONE,
backupRestoreState = BackupRestoreState.None,
contentCallbacks = ContentCallbacks.Empty,
@@ -10,6 +10,7 @@ import android.content.Intent
import android.os.Bundle
import androidx.activity.OnBackPressedCallback
import androidx.activity.viewModels
import androidx.core.view.WindowInsetsCompat
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.MainActivity
@@ -23,6 +24,7 @@ import org.thoughtcrime.securesms.logsubmit.SubmitDebugLogActivity
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme
import org.thoughtcrime.securesms.util.DynamicTheme
import org.thoughtcrime.securesms.util.SignalE164Util
import org.thoughtcrime.securesms.util.SystemWindowInsetsSetter
/**
* A captive activity that can determine if an interrupted/erred change number request
@@ -58,6 +60,8 @@ class ChangeNumberLockActivity : PassphraseRequiredActivity() {
setContentView(R.layout.activity_change_number_lock)
SystemWindowInsetsSetter.attach(findViewById(R.id.change_number_lock_scroll), this, WindowInsetsCompat.Type.systemBars())
reattemptChange()
}
@@ -28,7 +28,6 @@ import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.backup.isIdle
import org.thoughtcrime.securesms.backup.v2.ui.status.BackupCreationProgressRow
import org.thoughtcrime.securesms.components.compose.rememberBiometricsAuthentication
import org.thoughtcrime.securesms.compose.rememberStatusBarColorNestedScrollModifier
import org.thoughtcrime.securesms.keyvalue.protos.LocalBackupCreationProgress
import org.thoughtcrime.securesms.util.navigation.safeNavigate
@@ -162,7 +161,6 @@ private fun ChatsSettingsScreen(
LazyColumn(
modifier = Modifier
.padding(paddingValues)
.then(rememberStatusBarColorNestedScrollModifier())
) {
item {
Rows.ToggleRow(
@@ -27,7 +27,6 @@ import org.signal.core.ui.compose.Texts
import org.signal.core.util.bytes
import org.signal.mediasend.SentMediaQuality
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.compose.rememberStatusBarColorNestedScrollModifier
import org.thoughtcrime.securesms.util.AttachmentUtil
import org.thoughtcrime.securesms.util.navigation.safeNavigate
import org.thoughtcrime.securesms.webrtc.CallDataMode
@@ -135,7 +134,6 @@ private fun DataAndStorageSettingsScreen(
LazyColumn(
modifier = Modifier
.padding(paddingValues)
.then(rememberStatusBarColorNestedScrollModifier())
) {
item {
Rows.TextRow(
@@ -25,7 +25,6 @@ import org.signal.core.ui.BottomSheetUtil
import org.signal.core.ui.permissions.PermissionDeniedBottomSheet
import org.signal.core.ui.permissions.RationaleDialog
import org.signal.core.util.AppUtil
import org.signal.core.util.ThreadUtil
import org.signal.core.util.Util
import org.signal.core.util.concurrent.SignalExecutors
import org.signal.core.util.concurrent.SimpleTask
@@ -777,6 +776,50 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
}
)
switchPref(
title = DSLSettingsText.from("Set Video Config:"),
isChecked = state.callingSetVideoConfig,
onClick = {
viewModel.setInternalCallingSetVideoConfig(!state.callingSetVideoConfig)
}
)
switchPref(
title = DSLSettingsText.from(" Use Hardware Vp9 Encode"),
isChecked = state.callingUseHardwareVp9Encode,
isEnabled = state.callingSetVideoConfig,
onClick = {
viewModel.setInternalCallingUseHardwareVp9Encode(!state.callingUseHardwareVp9Encode)
}
)
switchPref(
title = DSLSettingsText.from(" Use Hardware Vp9 Decode"),
isChecked = state.callingUseHardwareVp9Decode,
isEnabled = state.callingSetVideoConfig,
onClick = {
viewModel.setInternalCallingUseHardwareVp9Decode(!state.callingUseHardwareVp9Decode)
}
)
switchPref(
title = DSLSettingsText.from(" Use Software Vp9 Encode"),
isChecked = state.callingUseSoftwareVp9Encode,
isEnabled = state.callingSetVideoConfig,
onClick = {
viewModel.setInternalCallingUseSoftwareVp9Encode(!state.callingUseSoftwareVp9Encode)
}
)
switchPref(
title = DSLSettingsText.from(" Use Software Vp9 Decode"),
isChecked = state.callingUseSoftwareVp9Decode,
isEnabled = state.callingSetVideoConfig,
onClick = {
viewModel.setInternalCallingUseSoftwareVp9Decode(!state.callingUseSoftwareVp9Decode)
}
)
dividerPref()
// TODO [alex] -- db access on main thread!
@@ -1055,19 +1098,17 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
.setTitle("Unregister?")
.setMessage("Are you sure? You'll have to re-register to use Signal again -- no promises that the process will go smoothly.")
.setPositiveButton(android.R.string.ok) { _, _ ->
AdvancedPrivacySettingsRepository(requireContext()).disablePushMessages {
ThreadUtil.runOnMain {
when (it) {
AdvancedPrivacySettingsRepository.DisablePushMessagesResult.SUCCESS -> {
SignalStore.account.setRegistered(false)
SignalStore.registration.clearRegistrationComplete()
SignalStore.registration.hasUploadedProfile = false
Toast.makeText(context, "Unregistered!", Toast.LENGTH_SHORT).show()
}
lifecycleScope.launch {
when (AdvancedPrivacySettingsRepository(requireContext()).disablePushMessages()) {
AdvancedPrivacySettingsRepository.DisablePushMessagesResult.SUCCESS -> {
SignalStore.account.setRegistered(false)
SignalStore.registration.clearRegistrationComplete()
SignalStore.registration.hasUploadedProfile = false
Toast.makeText(context, "Unregistered!", Toast.LENGTH_SHORT).show()
}
AdvancedPrivacySettingsRepository.DisablePushMessagesResult.NETWORK_ERROR -> {
Toast.makeText(context, "Network error!", Toast.LENGTH_SHORT).show()
}
AdvancedPrivacySettingsRepository.DisablePushMessagesResult.NETWORK_ERROR -> {
Toast.makeText(context, "Network error!", Toast.LENGTH_SHORT).show()
}
}
}
@@ -19,6 +19,11 @@ data class InternalSettingsState(
val callingUseSoftwareNs: Boolean,
val callingUseInputLowLatency: Boolean,
val callingUseInputVoiceComm: Boolean,
val callingSetVideoConfig: Boolean,
val callingUseHardwareVp9Encode: Boolean,
val callingUseHardwareVp9Decode: Boolean,
val callingUseSoftwareVp9Encode: Boolean,
val callingUseSoftwareVp9Decode: Boolean,
val useBuiltInEmojiSet: Boolean,
val emojiVersion: EmojiFiles.Version?,
val removeSenderKeyMinimium: Boolean,
@@ -142,6 +142,31 @@ class InternalSettingsViewModel(private val repository: InternalSettingsReposito
refresh()
}
fun setInternalCallingSetVideoConfig(enabled: Boolean) {
preferenceDataStore.putBoolean(InternalValues.CALLING_SET_VIDEO_CONFIG, enabled)
refresh()
}
fun setInternalCallingUseHardwareVp9Encode(enabled: Boolean) {
preferenceDataStore.putBoolean(InternalValues.CALLING_USE_HARDWARE_VP9_ENCODE, enabled)
refresh()
}
fun setInternalCallingUseHardwareVp9Decode(enabled: Boolean) {
preferenceDataStore.putBoolean(InternalValues.CALLING_USE_HARDWARE_VP9_DECODE, enabled)
refresh()
}
fun setInternalCallingUseSoftwareVp9Encode(enabled: Boolean) {
preferenceDataStore.putBoolean(InternalValues.CALLING_USE_SOFTWARE_VP9_ENCODE, enabled)
refresh()
}
fun setInternalCallingUseSoftwareVp9Decode(enabled: Boolean) {
preferenceDataStore.putBoolean(InternalValues.CALLING_USE_SOFTWARE_VP9_DECODE, enabled)
refresh()
}
fun setUseConversationItemV2Media(enabled: Boolean) {
SignalStore.internal.useConversationItemV2Media = enabled
refresh()
@@ -228,6 +253,11 @@ class InternalSettingsViewModel(private val repository: InternalSettingsReposito
callingUseSoftwareNs = SignalStore.internal.callingUseSoftwareNs,
callingUseInputLowLatency = SignalStore.internal.callingUseInputLowLatency,
callingUseInputVoiceComm = SignalStore.internal.callingUseInputVoiceComm,
callingSetVideoConfig = SignalStore.internal.callingSetVideoConfig,
callingUseHardwareVp9Encode = SignalStore.internal.callingUseHardwareVp9Encode,
callingUseHardwareVp9Decode = SignalStore.internal.callingUseHardwareVp9Decode,
callingUseSoftwareVp9Encode = SignalStore.internal.callingUseSoftwareVp9Encode,
callingUseSoftwareVp9Decode = SignalStore.internal.callingUseSoftwareVp9Decode,
useBuiltInEmojiSet = SignalStore.internal.forceBuiltInEmoji,
emojiVersion = null,
removeSenderKeyMinimium = SignalStore.internal.removeSenderKeyMinimum,
@@ -324,6 +324,12 @@ private fun StorageRecordRow(record: SignalStorageRecord) {
ManifestItemRow("ID", Hex.toStringCondensed(record.id.raw))
}
}
record.proto.stickerPack != null -> {
Column {
Text("Sticker Pack", fontWeight = FontWeight.Bold)
ManifestItemRow("ID", Hex.toStringCondensed(record.id.raw))
}
}
else -> {
Column {
Text("Unknown!")
@@ -13,4 +13,5 @@ sealed interface LabsSettingsEvents {
data class ToggleStarredMessages(val enabled: Boolean) : LabsSettingsEvents
data class ToggleStickerReplies(val enabled: Boolean) : LabsSettingsEvents
data class ToggleMuteBreakthroughNotifications(val enabled: Boolean) : LabsSettingsEvents
data class ToggleImprovedMessageDeletion(val enabled: Boolean) : LabsSettingsEvents
}
@@ -151,6 +151,15 @@ private fun LabsSettingsContent(
onCheckChanged = { onEvent(LabsSettingsEvents.ToggleMuteBreakthroughNotifications(it)) }
)
}
item {
Rows.ToggleRow(
checked = state.improvedMessageDeletion,
text = "Improved message deletion",
label = "Show a temporary tombstone when you delete a message, allowing you to promote it to a remote deletion.",
onCheckChanged = { onEvent(LabsSettingsEvents.ToggleImprovedMessageDeletion(it)) }
)
}
}
}
}
@@ -15,5 +15,6 @@ data class LabsSettingsState(
val betterSearch: Boolean = false,
val starredMessages: Boolean = false,
val stickerReplies: Boolean = false,
val muteBreakthroughNotifications: Boolean = false
val muteBreakthroughNotifications: Boolean = false,
val improvedMessageDeletion: Boolean = false
)
@@ -45,6 +45,10 @@ class LabsSettingsViewModel : ViewModel() {
SignalStore.labs.muteBreakthroughNotifications = event.enabled
_state.value = _state.value.copy(muteBreakthroughNotifications = event.enabled)
}
is LabsSettingsEvents.ToggleImprovedMessageDeletion -> {
SignalStore.labs.improvedMessageDeletion = event.enabled
_state.value = _state.value.copy(improvedMessageDeletion = event.enabled)
}
}
}
@@ -56,7 +60,8 @@ class LabsSettingsViewModel : ViewModel() {
betterSearch = SignalStore.labs.betterSearch,
starredMessages = SignalStore.labs.starredMessages,
stickerReplies = SignalStore.labs.stickerReplies,
muteBreakthroughNotifications = SignalStore.labs.muteBreakthroughNotifications
muteBreakthroughNotifications = SignalStore.labs.muteBreakthroughNotifications,
improvedMessageDeletion = SignalStore.labs.improvedMessageDeletion
)
}
}
@@ -39,7 +39,6 @@ import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.emoji.EmojiStrings
import org.thoughtcrime.securesms.components.settings.app.notifications.profiles.AddAllowedMembersViewModel.NotificationProfileAndRecipients
import org.thoughtcrime.securesms.compose.rememberStatusBarColorNestedScrollModifier
import org.thoughtcrime.securesms.database.RecipientTable
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfileId
@@ -168,7 +167,6 @@ private fun AddAllowedMembersContent(
LazyColumn(
modifier = Modifier
.weight(1f)
.then(rememberStatusBarColorNestedScrollModifier())
) {
item {
Text(
@@ -43,7 +43,6 @@ import org.signal.core.ui.compose.Scaffolds
import org.signal.core.ui.compose.SignalIcons
import org.signal.core.ui.compose.Texts
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.compose.rememberStatusBarColorNestedScrollModifier
import org.thoughtcrime.securesms.util.CommunicationActions
import org.thoughtcrime.securesms.util.viewModel
@@ -193,7 +192,6 @@ private fun AdvancedPrivacySettingsScreen(
LazyColumn(
modifier = Modifier
.padding(paddingValues)
.then(rememberStatusBarColorNestedScrollModifier())
) {
item {
Rows.ToggleRow(
@@ -3,8 +3,12 @@ package org.thoughtcrime.securesms.components.settings.app.privacy.advanced
import android.content.Context
import com.google.android.gms.tasks.Tasks
import com.google.firebase.installations.FirebaseInstallations
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.signal.core.util.concurrent.SignalExecutors
import org.signal.core.util.logging.Log
import org.signal.libsignal.net.DeviceDeregisteredException
import org.signal.libsignal.net.RequestResult
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.jobs.MultiDeviceConfigurationUpdateJob
@@ -13,8 +17,6 @@ import org.thoughtcrime.securesms.net.SignalNetwork
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.storage.StorageSyncHelper
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.whispersystems.signalservice.api.NetworkResultUtil
import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException
import java.io.IOException
import java.util.concurrent.ExecutionException
@@ -22,30 +24,34 @@ private val TAG = Log.tag(AdvancedPrivacySettingsRepository::class.java)
class AdvancedPrivacySettingsRepository(private val context: Context) {
fun disablePushMessages(consumer: (DisablePushMessagesResult) -> Unit) {
SignalExecutors.BOUNDED.execute {
val result = try {
try {
NetworkResultUtil.toBasicLegacy(SignalNetwork.account.clearFcmToken())
} catch (e: AuthorizationFailedException) {
Log.w(TAG, e)
}
if (SignalStore.account.fcmEnabled) {
Tasks.await(FirebaseInstallations.getInstance().delete())
}
DisablePushMessagesResult.SUCCESS
} catch (ioe: IOException) {
Log.w(TAG, ioe)
DisablePushMessagesResult.NETWORK_ERROR
} catch (e: InterruptedException) {
Log.w(TAG, "Interrupted while deleting", e)
DisablePushMessagesResult.NETWORK_ERROR
} catch (e: ExecutionException) {
Log.w(TAG, "Error deleting", e.cause)
DisablePushMessagesResult.NETWORK_ERROR
}
suspend fun disablePushMessages(): DisablePushMessagesResult = withContext(Dispatchers.IO) {
val clearTokenError: Throwable? = when (val result = SignalNetwork.account.clearFcmToken()) {
is RequestResult.Success, is RequestResult.NonSuccess -> null
is RequestResult.RetryableNetworkError -> result.networkError
is RequestResult.ApplicationError -> result.cause
}
consumer(result)
if (clearTokenError != null) {
Log.w(TAG, clearTokenError)
if (clearTokenError !is DeviceDeregisteredException) {
return@withContext DisablePushMessagesResult.NETWORK_ERROR
}
}
try {
if (SignalStore.account.fcmEnabled) {
Tasks.await(FirebaseInstallations.getInstance().delete())
}
DisablePushMessagesResult.SUCCESS
} catch (ioe: IOException) {
Log.w(TAG, ioe)
DisablePushMessagesResult.NETWORK_ERROR
} catch (e: InterruptedException) {
Log.w(TAG, "Interrupted while deleting", e)
DisablePushMessagesResult.NETWORK_ERROR
} catch (e: ExecutionException) {
Log.w(TAG, "Error deleting", e.cause)
DisablePushMessagesResult.NETWORK_ERROR
}
}
@@ -1,7 +1,6 @@
package org.thoughtcrime.securesms.components.settings.app.privacy.pnp
import android.content.res.Configuration
import android.os.Bundle
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
@@ -14,7 +13,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
@@ -34,23 +32,11 @@ import org.signal.core.ui.compose.Snackbars
import org.signal.core.ui.compose.Texts
import org.signal.core.ui.compose.showSnackbar
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.compose.StatusBarColorNestedScrollConnection
import org.signal.core.ui.R as CoreUiR
class PhoneNumberPrivacySettingsFragment : ComposeFragment() {
private val viewModel: PhoneNumberPrivacySettingsViewModel by viewModels()
private lateinit var statusBarNestedScrollConnection: StatusBarColorNestedScrollConnection
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
statusBarNestedScrollConnection = StatusBarColorNestedScrollConnection(requireActivity())
}
override fun onResume() {
super.onResume()
statusBarNestedScrollConnection.setColorImmediate()
}
@Composable
override fun FragmentContent() {
@@ -62,7 +48,6 @@ class PhoneNumberPrivacySettingsFragment : ComposeFragment() {
state = state,
snackbarHostState = snackbarHostState,
onNavigationClick = { findNavController().popBackStack() },
statusBarNestedScrollConnection = statusBarNestedScrollConnection,
onEveryoneCanSeeMyNumberClicked = viewModel::setEveryoneCanSeeMyNumber,
onNobodyCanSeeMyNumberClicked = viewModel::setNobodyCanSeeMyNumber,
onEveryoneCanFindMeByNumberClicked = viewModel::setEveryoneCanFindMeByMyNumber,
@@ -96,7 +81,6 @@ private fun Screen(
state: PhoneNumberPrivacySettingsState,
snackbarHostState: SnackbarHostState = SnackbarHostState(),
onNavigationClick: () -> Unit = {},
statusBarNestedScrollConnection: StatusBarColorNestedScrollConnection? = null,
onEveryoneCanSeeMyNumberClicked: () -> Unit = {},
onNobodyCanSeeMyNumberClicked: () -> Unit = {},
onEveryoneCanFindMeByNumberClicked: () -> Unit = {},
@@ -109,8 +93,7 @@ private fun Screen(
navigationContentDescription = stringResource(id = R.string.Material3SearchToolbar__close),
snackbarHost = {
SnackbarHost(snackbarHostState)
},
modifier = statusBarNestedScrollConnection?.let { Modifier.nestedScroll(it) } ?: Modifier
}
) { contentPadding ->
Box(modifier = Modifier.padding(contentPadding)) {
LazyColumn {
@@ -334,12 +334,14 @@ private fun ManageStorageSettingsScreen(
onClick = onSetChatLengthLimit
)
Rows.ToggleRow(
text = stringResource(id = R.string.ManageStorageSettingsFragment_apply_limits_title),
label = stringResource(id = R.string.ManageStorageSettingsFragment_apply_limits_description),
checked = state.syncTrimDeletes,
onCheckChanged = onSyncTrimThreadDeletes
)
if (state.isPrimary) {
Rows.ToggleRow(
text = stringResource(id = R.string.ManageStorageSettingsFragment_apply_limits_title),
label = stringResource(id = R.string.ManageStorageSettingsFragment_apply_limits_description),
checked = state.syncTrimDeletes,
onCheckChanged = onSyncTrimThreadDeletes
)
}
Dividers.Default()
@@ -36,7 +36,8 @@ class ManageStorageSettingsViewModel : ViewModel() {
keepMessagesDuration = SignalStore.settings.keepMessagesDuration,
lengthLimit = if (SignalStore.settings.isTrimByLengthEnabled) SignalStore.settings.threadTrimLength else ManageStorageState.NO_LIMIT,
syncTrimDeletes = SignalStore.settings.shouldSyncThreadTrimDeletes(),
localBackupsEnabled = SignalStore.backup.newLocalBackupsEnabled
localBackupsEnabled = SignalStore.backup.newLocalBackupsEnabled,
isPrimary = SignalStore.account.isPrimaryDevice
)
)
val state = store.asStateFlow()
@@ -182,7 +183,8 @@ class ManageStorageSettingsViewModel : ViewModel() {
val onDeviceStorageOptimizationState: OnDeviceStorageOptimizationState = OnDeviceStorageOptimizationState.FEATURE_NOT_AVAILABLE,
val storageOptimizationStateChanged: Boolean = false,
val isPaidTierPending: Boolean = false,
val localBackupsEnabled: Boolean = false
val localBackupsEnabled: Boolean = false,
val isPrimary: Boolean = true
) {
companion object {
const val NO_LIMIT = 0
@@ -94,7 +94,8 @@ object RecurringInAppPaymentRepository {
} ?: return NetworkResult.Success(ActiveSubscription.EMPTY)
response.result.ifPresent { result ->
if (result.isActive && result.activeSubscription.endOfCurrentPeriod > SignalStore.inAppPayments.getLastEndOfPeriod()) {
val lastEndOfPeriod = SignalDatabase.inAppPayments.getByLatestEndOfPeriod(type.inAppPaymentType)?.endOfPeriodSeconds ?: 0L
if (result.isActive && result.activeSubscription.endOfCurrentPeriod > lastEndOfPeriod) {
InAppPaymentKeepAliveJob.enqueueAndTrackTime(System.currentTimeMillis().milliseconds)
}
}
@@ -115,8 +115,8 @@ class DonateToSignalFragment :
override fun getMaterial3OnScrollHelper(toolbar: Toolbar?): Material3OnScrollHelper {
return object : Material3OnScrollHelper(activity = requireActivity(), views = listOf(toolbar!!), lifecycleOwner = viewLifecycleOwner) {
override val activeColorSet: ColorSet = ColorSet(R.color.transparent, CoreUiR.color.signal_colorBackground)
override val inactiveColorSet: ColorSet = ColorSet(R.color.transparent, CoreUiR.color.signal_colorBackground)
override val activeColorSet: ColorSet = ColorSet(R.color.transparent)
override val inactiveColorSet: ColorSet = ColorSet(R.color.transparent)
}
}
@@ -3,9 +3,12 @@ package org.thoughtcrime.securesms.components.settings.app.subscription.donate.c
import android.content.Context
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import androidx.annotation.StringRes
import androidx.core.content.ContextCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updateLayoutParams
import androidx.core.widget.addTextChangedListener
import androidx.fragment.app.Fragment
import androidx.fragment.app.setFragmentResult
@@ -33,6 +36,7 @@ import org.thoughtcrime.securesms.components.settings.app.subscription.donate.st
import org.thoughtcrime.securesms.database.InAppPaymentTable
import org.thoughtcrime.securesms.databinding.CreditCardFragmentBinding
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
import org.thoughtcrime.securesms.util.SystemWindowInsetsSetter
import org.thoughtcrime.securesms.util.ViewUtil
import org.thoughtcrime.securesms.util.navigation.safeNavigate
import org.thoughtcrime.securesms.util.viewModel
@@ -55,6 +59,10 @@ class CreditCardFragment : Fragment(R.layout.credit_card_fragment), InAppPayment
TemporaryScreenshotSecurity.bindToViewLifecycleOwner(this)
InAppPaymentCheckoutDelegate.ErrorHandler().attach(this, this, args.inAppPaymentId)
binding.toolbar.updateLayoutParams { height = ViewGroup.LayoutParams.WRAP_CONTENT }
SystemWindowInsetsSetter.attach(binding.toolbar, viewLifecycleOwner, WindowInsetsCompat.Type.statusBars())
SystemWindowInsetsSetter.attach(binding.continueButton, viewLifecycleOwner, WindowInsetsCompat.Type.navigationBars(), SystemWindowInsetsSetter.ApplyMode.MARGIN)
setFragmentResultListener(StripePaymentInProgressFragment.REQUEST_KEY) { _, bundle ->
val result: InAppPaymentProcessorActionResult = bundle.getParcelableCompat(StripePaymentInProgressFragment.REQUEST_KEY, InAppPaymentProcessorActionResult::class.java)!!
if (result.status == InAppPaymentProcessorActionResult.Status.SUCCESS) {
@@ -1,6 +1,7 @@
package org.thoughtcrime.securesms.components.settings.app.subscription.donate.paypal
import android.annotation.SuppressLint
import android.app.Dialog
import android.content.DialogInterface
import android.graphics.Bitmap
import android.os.Bundle
@@ -15,12 +16,14 @@ import androidx.fragment.app.setFragmentResult
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.navigation.fragment.navArgs
import org.signal.core.ui.enableEdgeToEdge
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.ViewBinderDelegate
import org.thoughtcrime.securesms.components.settings.app.subscription.PayPalRepository
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationWebViewOnBackPressedCallback
import org.thoughtcrime.securesms.databinding.DonationWebviewFragmentBinding
import org.thoughtcrime.securesms.util.SystemWindowInsetsSetter
import org.thoughtcrime.securesms.util.visible
/**
@@ -49,8 +52,16 @@ class PayPalConfirmationDialogFragment : DialogFragment(R.layout.donation_webvie
setStyle(STYLE_NO_FRAME, R.style.Signal_DayNight_Dialog_FullScreen)
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return super.onCreateDialog(savedInstanceState).apply {
window?.enableEdgeToEdge()
}
}
@SuppressLint("SetJavaScriptEnabled")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
SystemWindowInsetsSetter.attach(view, viewLifecycleOwner)
val client = PayPalWebClient()
viewLifecycleOwner.lifecycle.addObserver(client)
binding.webView.webViewClient = client
@@ -1,6 +1,7 @@
package org.thoughtcrime.securesms.components.settings.app.subscription.donate.stripe
import android.annotation.SuppressLint
import android.app.Dialog
import android.content.DialogInterface
import android.content.Intent
import android.graphics.Bitmap
@@ -24,6 +25,7 @@ import com.google.android.material.button.MaterialButton
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.signal.core.ui.enableEdgeToEdge
import org.signal.core.util.concurrent.LifecycleDisposable
import org.signal.donations.StripeIntentAccessor
import org.thoughtcrime.securesms.R
@@ -35,6 +37,7 @@ import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
import org.thoughtcrime.securesms.databinding.DonationWebviewFragmentBinding
import org.thoughtcrime.securesms.util.Environment
import org.thoughtcrime.securesms.util.RemoteConfig
import org.thoughtcrime.securesms.util.SystemWindowInsetsSetter
import org.thoughtcrime.securesms.util.visible
/**
@@ -64,9 +67,16 @@ class Stripe3DSDialogFragment : DialogFragment(R.layout.donation_webview_fragmen
setStyle(STYLE_NO_FRAME, R.style.Signal_DayNight_Dialog_FullScreen)
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return super.onCreateDialog(savedInstanceState).apply {
window?.enableEdgeToEdge()
}
}
@SuppressLint("SetJavaScriptEnabled", "SetTextI18n")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
lifecycleDisposable.bindTo(viewLifecycleOwner)
SystemWindowInsetsSetter.attach(view, viewLifecycleOwner)
dialog!!.window!!.setFlags(
WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED,
@@ -9,9 +9,11 @@ import android.os.Bundle
import android.view.View
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.text.KeyboardActions
@@ -231,6 +233,8 @@ fun BankTransferDetailsContent(
.fillMaxWidth()
.fillMaxHeight()
.padding(it)
.consumeWindowInsets(it)
.imePadding()
) {
val focusManager = LocalFocusManager.current
val focusRequester = remember { FocusRequester() }
@@ -9,8 +9,10 @@ import android.os.Bundle
import android.view.View
import androidx.annotation.StringRes
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.text.KeyboardActions
@@ -237,7 +239,10 @@ private fun IdealTransferDetailsContent(
Column(
horizontalAlignment = CenterHorizontally,
modifier = Modifier.padding(it)
modifier = Modifier
.padding(it)
.consumeWindowInsets(it)
.imePadding()
) {
LazyColumn(
modifier = Modifier
@@ -5,9 +5,6 @@
package org.thoughtcrime.securesms.components.settings.app.subscription.donate.transfer.mandate
import android.annotation.SuppressLint
import android.os.Bundle
import android.view.View
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
@@ -60,7 +57,6 @@ import org.signal.core.ui.compose.Texts
import org.signal.core.ui.compose.theme.SignalTheme
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.transfer.DonationTransferTestTags
import org.thoughtcrime.securesms.compose.StatusBarColorAnimator
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
import org.thoughtcrime.securesms.util.SpanUtil
import org.thoughtcrime.securesms.util.navigation.safeNavigate
@@ -76,17 +72,6 @@ class BankTransferMandateFragment : ComposeFragment() {
BankTransferMandateViewModel(args.inAppPaymentId)
}
private lateinit var statusBarColorAnimator: StatusBarColorAnimator
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
statusBarColorAnimator = StatusBarColorAnimator(requireActivity())
}
override fun onResume() {
super.onResume()
statusBarColorAnimator.setColorImmediate()
}
@Composable
override fun FragmentContent() {
val mandate by viewModel.mandate
@@ -97,8 +82,7 @@ class BankTransferMandateFragment : ComposeFragment() {
failedToLoadMandate = failedToLoadMandate,
onNavigationClick = this::onNavigationClick,
onContinueClick = this::onContinueClick,
onLearnMoreClick = this::onLearnMoreClick,
onCanScrollUp = statusBarColorAnimator::setCanScrollUp
onLearnMoreClick = this::onLearnMoreClick
)
}
@@ -136,22 +120,19 @@ fun BankTransferScreenPreview() {
failedToLoadMandate = false,
onNavigationClick = {},
onContinueClick = {},
onLearnMoreClick = {},
onCanScrollUp = {}
onLearnMoreClick = {}
)
}
}
@OptIn(ExperimentalMaterial3Api::class)
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@Composable
fun BankTransferScreen(
bankMandate: String,
failedToLoadMandate: Boolean,
onNavigationClick: () -> Unit,
onContinueClick: () -> Unit,
onLearnMoreClick: () -> Unit,
onCanScrollUp: (Boolean) -> Unit
onLearnMoreClick: () -> Unit
) {
val listState = rememberLazyListState()
val scope = rememberCoroutineScope()
@@ -182,17 +163,19 @@ fun BankTransferScreen(
colors = if (listState.canScrollBackward) TopAppBarDefaults.topAppBarColors(containerColor = SignalTheme.colors.colorSurface2) else TopAppBarDefaults.topAppBarColors()
)
}
) {
onCanScrollUp(listState.canScrollBackward)
Column(horizontalAlignment = CenterHorizontally, modifier = Modifier.fillMaxSize()) {
) { paddingValues ->
Column(
horizontalAlignment = CenterHorizontally,
modifier = Modifier
.padding(paddingValues)
.fillMaxSize()
) {
LazyColumn(
state = listState,
horizontalAlignment = CenterHorizontally,
modifier = Modifier
.fillMaxWidth()
.weight(1f, true)
.padding(top = 64.dp)
) {
item {
Image(
@@ -138,8 +138,8 @@ class ManageDonationsFragment :
override fun getMaterial3OnScrollHelper(toolbar: Toolbar?): Material3OnScrollHelper {
return object : Material3OnScrollHelper(activity = requireActivity(), views = listOf(toolbar!!), lifecycleOwner = viewLifecycleOwner) {
override val activeColorSet: ColorSet = ColorSet(R.color.transparent, CoreUiR.color.signal_colorBackground)
override val inactiveColorSet: ColorSet = ColorSet(R.color.transparent, CoreUiR.color.signal_colorBackground)
override val activeColorSet: ColorSet = ColorSet(R.color.transparent)
override val inactiveColorSet: ColorSet = ColorSet(R.color.transparent)
}
}
@@ -22,7 +22,6 @@ import org.signal.core.ui.compose.Rows
import org.signal.core.ui.compose.Scaffolds
import org.signal.core.ui.compose.SignalIcons
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.compose.rememberStatusBarColorNestedScrollModifier
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.jobs.ApkUpdateJob
import org.thoughtcrime.securesms.keyvalue.SignalStore
@@ -92,7 +91,6 @@ private fun AppUpdatesSettingsScreen(
LazyColumn(
modifier = Modifier
.padding(paddingValues)
.then(rememberStatusBarColorNestedScrollModifier())
) {
if (Build.VERSION.SDK_INT >= 31) {
item {
@@ -9,7 +9,7 @@ import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.WindowManager
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContract
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
@@ -29,7 +29,7 @@ class QrImageSelectionActivity : AppCompatActivity(), MediaGalleryFragment.Callb
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
window.addFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS or WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN)
enableEdgeToEdge()
setContentView(R.layout.username_qr_image_selection_activity)
}
@@ -14,6 +14,7 @@ import android.content.Intent
import android.os.Bundle
import android.widget.Toast
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContract
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
@@ -67,6 +68,7 @@ class UsernameQrScannerActivity : AppCompatActivity() {
}
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
val galleryLauncher = registerForActivityResult(QrImageSelectionActivity.Contract()) { uri ->
@@ -29,7 +29,6 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import io.reactivex.rxjava3.kotlin.subscribeBy
import kotlinx.coroutines.launch
import org.signal.core.ui.isSplitPane
import org.signal.core.ui.permissions.Permissions
import org.signal.core.util.DimensionUnit
import org.signal.core.util.Result
@@ -117,7 +116,6 @@ import org.thoughtcrime.securesms.util.ExpirationUtil
import org.thoughtcrime.securesms.util.Material3OnScrollHelper
import org.thoughtcrime.securesms.util.RemoteConfig
import org.thoughtcrime.securesms.util.ViewUtil
import org.thoughtcrime.securesms.util.WindowUtil
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
import org.thoughtcrime.securesms.util.navigation.safeNavigate
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog
@@ -274,16 +272,10 @@ class ConversationSettingsFragment :
return object : Material3OnScrollHelper(
activity = requireActivity(),
views = listOf(toolbar!!),
lifecycleOwner = viewLifecycleOwner,
setStatusBarColor = { color ->
if (!resources.isSplitPane() || activity is ConversationSettingsActivity) {
WindowUtil.setStatusBarColor(requireActivity().window, color)
}
}
lifecycleOwner = viewLifecycleOwner
) {
override val inactiveColorSet = ColorSet(
toolbarColorRes = CoreUiR.color.signal_colorBackground_0,
statusBarColorRes = CoreUiR.color.signal_colorBackground
toolbarColorRes = CoreUiR.color.signal_colorBackground_0
)
}
}
@@ -141,7 +141,7 @@ class ConversationSettingsRepository(
SignalExecutors.BOUNDED.execute {
val groupRecord: GroupRecord = SignalDatabase.groups.getGroup(groupId).get()
consumer(
if (groupRecord.isV2Group) {
if (groupRecord.hasV2GroupProperties) {
val decryptedGroup: DecryptedGroup = groupRecord.requireV2GroupProperties().decryptedGroup
val pendingMembers: List<RecipientId> = decryptedGroup.pendingMembers
.map { m -> m.serviceIdBytes }
@@ -65,7 +65,7 @@ class PermissionsSettingsRepository(
fun hasNonAdminMembersWithLabels(groupId: GroupId): Boolean {
val v2GroupId = groupId.v2OrNull() ?: return false
val group = groupTable.getGroup(v2GroupId).orNull() ?: return false
val group = groupTable.getGroup(v2GroupId).filter { it.hasV2GroupProperties }.orNull() ?: return false
return group.requireV2GroupProperties().nonAdminMembersWithLabels().isNotEmpty()
}
@@ -6,10 +6,10 @@ import android.text.SpannableStringBuilder
import android.view.View
import android.widget.TextView
import android.widget.Toast
import org.signal.core.ui.fonts.SignalSymbols
import org.signal.core.util.ServiceUtil
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.PreferenceModel
import org.thoughtcrime.securesms.fonts.SignalSymbols
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
@@ -12,10 +12,10 @@ import android.widget.TextView
import androidx.constraintlayout.widget.Group
import com.google.android.material.button.MaterialButton
import com.google.android.material.card.MaterialCardView
import org.signal.core.ui.fonts.SignalSymbols
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.AvatarImageView
import org.thoughtcrime.securesms.components.webrtc.v2.PendingParticipantsListener
import org.thoughtcrime.securesms.fonts.SignalSymbols
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.service.webrtc.PendingParticipantCollection
import org.thoughtcrime.securesms.util.ViewUtil
@@ -134,6 +134,7 @@ fun CallScreen(
onRemoteMuteToastDismissed: () -> Unit = {},
isSelfAdmin: Boolean = false,
isCallLink: Boolean = false,
canRemoteMute: Boolean = false,
onMuteAudio: (CallParticipant) -> Unit = {},
onRemoveFromCall: (CallParticipant) -> Unit = {},
onContactDetails: (CallParticipant) -> Unit = {},
@@ -383,6 +384,7 @@ fun CallScreen(
participant = longPressedParticipant,
isSelfAdmin = isSelfAdmin,
isCallLink = isCallLink,
canRemoteMute = canRemoteMute,
onDismiss = { longPressedParticipantId = null },
onMuteAudio = onMuteAudio,
onRemoveFromCall = onRemoveFromCall,
@@ -587,6 +589,7 @@ private fun ParticipantContextMenu(
participant: CallParticipant?,
isSelfAdmin: Boolean,
isCallLink: Boolean,
canRemoteMute: Boolean,
onDismiss: () -> Unit,
onMuteAudio: (CallParticipant) -> Unit,
onRemoveFromCall: (CallParticipant) -> Unit,
@@ -620,7 +623,7 @@ private fun ParticipantContextMenu(
.background(color = MaterialTheme.colorScheme.surfaceVariant)
)
if (resolved.isMicrophoneEnabled) {
if (canRemoteMute && resolved.isMicrophoneEnabled) {
DropdownMenuItem(
text = { Text(stringResource(R.string.CallParticipantSheet__mute_audio)) },
leadingIcon = { Icon(painter = painterResource(R.drawable.symbol_mic_slash_24), contentDescription = null) },
@@ -683,6 +686,7 @@ private fun ParticipantContextMenuAdminPreview() {
),
isSelfAdmin = true,
isCallLink = true,
canRemoteMute = true,
onDismiss = {},
onMuteAudio = {},
onRemoveFromCall = {},
@@ -705,6 +709,7 @@ private fun ParticipantContextMenuNonAdminPreview() {
),
isSelfAdmin = false,
isCallLink = false,
canRemoteMute = true,
onDismiss = {},
onMuteAudio = {},
onRemoveFromCall = {},
@@ -50,7 +50,6 @@ import org.thoughtcrime.securesms.reactions.any.ReactWithAnyEmojiBottomSheetDial
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.service.webrtc.links.UpdateCallLinkResult
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcEphemeralState
import org.thoughtcrime.securesms.util.WindowUtil
import org.thoughtcrime.securesms.webrtc.CallParticipantsViewState
import kotlin.time.Duration.Companion.seconds
@@ -76,9 +75,6 @@ class ComposeCallScreenMediator(private val activity: WebRtcCallActivity, viewMo
private val lifecycleDisposable = LifecycleDisposable()
init {
WindowUtil.clearTranslucentNavigationBar(activity.window)
WindowUtil.clearTranslucentStatusBar(activity.window)
activity.enableEdgeToEdge(
statusBarStyle = SystemBarStyle.dark(Color.TRANSPARENT),
navigationBarStyle = SystemBarStyle.dark(Color.TRANSPARENT)
@@ -241,6 +237,7 @@ class ComposeCallScreenMediator(private val activity: WebRtcCallActivity, viewMo
callParticipantUpdatePopupController = callParticipantUpdatePopupController,
isSelfAdmin = controlAndInfoState.isSelfAdmin(),
isCallLink = controlAndInfoState.callLink != null,
canRemoteMute = callParticipantsState.groupCallState.isNotIdle,
onMuteAudio = callInfoCallbacks::onMuteAudio,
onRemoveFromCall = callInfoCallbacks::onRemoveFromCall,
onContactDetails = callInfoCallbacks::onContactDetails,
@@ -298,7 +298,7 @@ class WebRtcCallViewModel : ViewModel() {
@MainThread
fun updateFromWebRtcViewModel(webRtcViewModel: WebRtcViewModel, enableVideo: Boolean) {
canEnterPipMode.value = !webRtcViewModel.state.isPreJoinOrNetworkUnavailable
canEnterPipMode.value = !webRtcViewModel.state.isPreJoinOrNetworkUnavailable && !webRtcViewModel.isLocalScreenSharing
if (isCallStarting && webRtcViewModel.state.isPassedPreJoin) {
isCallStarting = false
}
@@ -1,62 +0,0 @@
package org.thoughtcrime.securesms.compose
import android.animation.ValueAnimator
import android.app.Activity
import androidx.core.content.ContextCompat
import com.google.android.material.animation.ArgbEvaluatorCompat
import org.thoughtcrime.securesms.util.WindowUtil
import org.signal.core.ui.R as CoreUiR
/**
* Controls status-bar color based off ability to scroll up
*/
class StatusBarColorAnimator(
private val activity: Activity
) {
private var animator: ValueAnimator? = null
private var previousCanScrollUp: Boolean = false
private val normalColor = ContextCompat.getColor(activity, CoreUiR.color.signal_colorBackground)
private val scrollColor = ContextCompat.getColor(activity, CoreUiR.color.signal_colorSurface2)
fun setCanScrollUp(canScrollUp: Boolean) {
if (previousCanScrollUp == canScrollUp) {
return
}
previousCanScrollUp = canScrollUp
applyState(canScrollUp)
}
fun setColorImmediate() {
val end = when {
previousCanScrollUp -> scrollColor
else -> normalColor
}
animator?.cancel()
WindowUtil.setStatusBarColor(
activity.window,
end
)
}
private fun applyState(canScrollUp: Boolean) {
val (start, end) = when {
canScrollUp -> normalColor to scrollColor
else -> scrollColor to normalColor
}
animator?.cancel()
animator = ValueAnimator.ofFloat(0f, 1f).apply {
duration = 200
addUpdateListener {
WindowUtil.setStatusBarColor(
activity.window,
ArgbEvaluatorCompat.getInstance().evaluate(it.animatedFraction, start, end)
)
}
start()
}
}
}

Some files were not shown because too many files have changed in this diff Show More