Compare commits

..

129 Commits

Author SHA1 Message Date
Cody Henthorne
9359d56880 Bump version to 8.0.3 2026-02-23 11:42:37 -05:00
Cody Henthorne
3214200188 Update baseline profile. 2026-02-23 11:35:58 -05:00
Cody Henthorne
841ab7f983 Update translations and other static files. 2026-02-23 11:06:18 -05:00
Alex Hart
53b3728432 Update handling for early nav. 2026-02-23 11:13:42 -04:00
Alex Hart
cf9f98efc9 Fix bad behavior when rotating device with message details open. 2026-02-23 10:54:21 -04:00
Alex Hart
b5c666a1f4 Bump version to 8.0.2 2026-02-19 13:18:41 -04:00
Alex Hart
b1954a509c Update baseline profile. 2026-02-19 13:16:06 -04:00
Alex Hart
c2c91cfe42 Update translations and other static files. 2026-02-19 12:51:41 -04:00
Greyson Parrelli
cccbec5744 Only set HDR transcoder flags for HDR content. 2026-02-19 10:58:55 -05:00
Alex Hart
4c89b20fad Fix possible captcha race. 2026-02-19 11:14:14 -04:00
Greyson Parrelli
2328fa3e88 Route video GIF attachments to the GENERIC_TRANSCODE queue. 2026-02-19 09:57:23 -05:00
Greyson Parrelli
e19d4624c1 Fix video transcoding crash caused by premature codec API calls.
Move getParameterDescriptor and setParameters calls to after
configure/start, since they require the codec to be in the
Executing state. Always set KEY_COLOR_TRANSFER_REQUEST in the
format before configure as the primary tone-mapping mechanism.
2026-02-19 09:57:06 -05:00
Alex Hart
345f58ed48 Bump version to 8.0.1 2026-02-18 16:26:31 -04:00
Alex Hart
4c14ce3937 Update translations and other static files. 2026-02-18 16:17:34 -04:00
Alex Hart
82684c0169 Bump version to 8.0.0 2026-02-18 16:03:34 -04:00
Alex Hart
2607328255 Update translations and other static files. 2026-02-18 15:52:21 -04:00
Michelle Tang
484ce3a1da Turn on binary service writes. 2026-02-18 15:48:16 -04:00
Greyson Parrelli
85d5f62301 Fix potential archive sync issue. 2026-02-18 15:48:16 -04:00
jeffrey-signal
b0571f8184 Fix edit group member label placeholder text. 2026-02-18 15:48:16 -04:00
Cody Henthorne
b80dd28b40 Defensively prevent gif playback in background. 2026-02-18 15:48:16 -04:00
Greyson Parrelli
e0cf0808cf PIP moves opposite to finger in RTL mode.
Co-authored-by: Alex Hart <alex@signal.org>
2026-02-18 15:48:16 -04:00
Greyson Parrelli
ffdd5b62ae Remove now-unused colorInfo parsing from video transcoding. 2026-02-18 15:48:16 -04:00
jeffrey-signal
3b5376ef8b Fix mixed-directional text input behavior in recipient search field. 2026-02-18 15:48:16 -04:00
jeffrey-signal
cd57fb0d76 Fix member label pill overflow caused by excessive combining marks. 2026-02-18 15:48:16 -04:00
adel-signal
6986acd6f4 Update RingRTC to 2.65.0
Co-authored-by: emir-signal <emir@signal.org>
Co-authored-by: Cody Henthorne <cody@signal.org>
2026-02-18 15:48:16 -04:00
Greyson Parrelli
2bc571ffd3 Possible fix for some transcoding issues. 2026-02-18 15:48:16 -04:00
Greyson Parrelli
a8dddf33f8 Media needs to be tapped on twice for it to load during call.
Fixes #14581
Fixes #14592
2026-02-18 15:48:16 -04:00
Greyson Parrelli
46582a685b Fix weird screen transition when accepting an incoming call.
Co-authored-by: Alex Hart <alex@signal.org>
2026-02-18 15:48:16 -04:00
Greyson Parrelli
ad381783f7 Gifs stop or lower volume of other media playing.
Fixes #14297

Co-authored-by: Alex Hart <alex@signal.org>
2026-02-18 15:48:16 -04:00
jeffrey-signal
b81c1eb65c Fix crash when selecting a group to add a recipient. 2026-02-18 15:48:16 -04:00
Greyson Parrelli
2c4d3b3ee4 Fix deleted attachment still visible under 'Shared media'. 2026-02-18 15:48:16 -04:00
Greyson Parrelli
d1400928ce Slide in country picker vertically in regV5. 2026-02-18 15:48:16 -04:00
Greyson Parrelli
49abece92b Includes quotes of your messages in 'notify for mentions' setting.
Fixes #14595
2026-02-18 15:48:16 -04:00
Greyson Parrelli
b48b1f031e Fix gradle format task. 2026-02-18 15:48:16 -04:00
Greyson Parrelli
9cefe0bc04 Allow importing a backup as part of quickstart. 2026-02-18 15:48:16 -04:00
Greyson Parrelli
ee73b0e229 Allow reading quickstart credentials from disk. 2026-02-18 15:48:16 -04:00
Greyson Parrelli
ab0ce58812 Fix find-by-username text field using wrong background color.
The TextField was not explicitly setting container colors, causing it
to fall back to Material 3 defaults instead of the app's surfaceVariant
color. Added focusedContainerColor, unfocusedContainerColor, and
disabledContainerColor to match the pattern used throughout the rest
of the codebase.
2026-02-18 15:48:16 -04:00
Greyson Parrelli
333a206d36 Fix issue where emoji search bar got weird after tab switching. 2026-02-18 15:48:16 -04:00
Michelle Tang
86bb7666ea Fix unpinning all in note to self. 2026-02-18 15:48:16 -04:00
Cody Henthorne
58b5ebf39d Allow SendDeliveryReceiptJob to run during benchmarking tests. 2026-02-18 15:48:16 -04:00
Greyson Parrelli
47947b85c7 Fix note to self media delete options. 2026-02-18 15:48:16 -04:00
Greyson Parrelli
6910ba6d2e Ignore storageId in Recipient.hasSameContent().
Was potentially causing unnecessary UI churn.
2026-02-18 15:48:16 -04:00
Cody Henthorne
08254edae6 Add incoming group message benchmark tests. 2026-02-18 15:48:16 -04:00
Greyson Parrelli
e67307a961 Add quickstart variant that launches with predefined credentials. 2026-02-18 15:48:16 -04:00
Cody Henthorne
9922621945 Add incoming individual message benchmark tests. 2026-02-18 15:48:16 -04:00
Greyson Parrelli
c7476a2a07 Mute in lobby if accepting large group call. 2026-02-18 15:48:16 -04:00
Alex Hart
ac59528f5c Enable call vanity when joining a video call. 2026-02-18 15:48:16 -04:00
Alex Hart
97c9728c65 Resolve crash in blur container due to multiple measures. 2026-02-18 15:48:16 -04:00
Greyson Parrelli
80d1694e6e Ensure data is updated when refreshing contacts during a search.
Fixes #14112
2026-02-18 15:48:16 -04:00
Greyson Parrelli
28c6e31c7d Removing a recipient does not immediately remove them from the contacts list. 2026-02-18 15:48:16 -04:00
Cody Henthorne
8836b2a570 Fix media send delayed after backgrounding app. 2026-02-18 15:48:16 -04:00
Greyson Parrelli
786c2b888b Remove beta labeling from Signal Secure Backups. 2026-02-18 15:48:16 -04:00
Greyson Parrelli
c91275c5da Add the country code picker to regV5. 2026-02-18 15:48:16 -04:00
Greyson Parrelli
7b362460e7 Spruce up the welcome and permission screens in regV5. 2026-02-18 15:48:16 -04:00
Greyson Parrelli
a1862c3420 Sticker from Google emoji keyboard renders as image attachment when forwarded. 2026-02-18 15:48:16 -04:00
jeffrey-signal
44ea9ccc59 Fix unique constraint violation when importing groups with duplicate recipient IDs. 2026-02-18 15:48:16 -04:00
andrew-signal
4c9cdf3b8f Refactor RefreshOwnProfileJob to Kotlin. 2026-02-18 15:48:16 -04:00
Greyson Parrelli
4a6d4f197d Add 429 handling to various archive calls. 2026-02-18 15:48:16 -04:00
Greyson Parrelli
ae04749336 Add better logging for ovesized messages. 2026-02-18 15:48:16 -04:00
Cody Henthorne
caa743aba2 Fix foreground service start in background crash by starting service sooner for incoming group calls. 2026-02-18 15:48:16 -04:00
Alex Hart
a4469a4285 Fix issue with initial audio output setting.
Co-authored-by: Greyson Parrelli <greyson@signal.org>
2026-02-18 15:48:08 -04:00
Alex Hart
2771b31aab Clear stale user-selected audio device when it disconnects.
Co-authored-by: Greyson Parrelli <greyson@signal.org>
2026-02-18 15:48:06 -04:00
Greyson Parrelli
5c418a4260 Add RRP support to regV5. 2026-02-18 15:48:06 -04:00
Alex Hart
17dbdf3b74 Bump version to 7.74.3 2026-02-18 11:38:55 -04:00
Alex Hart
8315ae47c4 Update translations and other static files. 2026-02-18 11:31:14 -04:00
Michelle Tang
50b59805ca Add more binary service reads. 2026-02-18 10:13:41 -05:00
Alex Hart
58bc387d0b Bump version to 7.74.2 2026-02-17 23:18:40 -04:00
Alex Hart
d2dd6790a0 Update translations and other static files. 2026-02-17 23:16:23 -04:00
jeffrey-signal
78c7795b89 Fix sender name/label overflowing beyond message bubble media width. 2026-02-17 22:43:06 -04:00
jeffrey-signal
73c25f3476 Ensure consistent margins below sender name/label. 2026-02-17 18:17:36 -05:00
jeffrey-signal
6f9f89734d Fix sender name/label overflowing beyond message bubble media width. 2026-02-17 16:10:33 -05:00
Greyson Parrelli
da68bee742 Fix issue where you can't download media in calls. 2026-02-17 11:44:28 -05:00
jeffrey-signal
e5f1f9394d Fix sender name alignment in conversations. 2026-02-17 11:37:07 -05:00
Michelle Tang
d9d6f8c97e Bump version to 7.74.1 2026-02-16 16:33:31 -05:00
Michelle Tang
a35a141411 Update translations and other static files. 2026-02-16 16:18:38 -05:00
Michelle Tang
1e0c7b1120 Fix recipient call crash. 2026-02-16 16:09:39 -05:00
Michelle Tang
9b9734f82a Bump version to 7.74.0 2026-02-11 18:24:13 -05:00
Michelle Tang
fe437b5234 Update translations and other static files. 2026-02-11 18:14:12 -05:00
jeffrey-signal
59bb505a3e Support member labels in backups. 2026-02-11 18:08:57 -05:00
jeffrey-signal
28d8d62cbd Rotate receive member labels flag. 2026-02-11 18:08:57 -05:00
jeffrey-signal
cb05608422 Show member labels in quotes. 2026-02-11 18:08:57 -05:00
Michelle Tang
e384a37fab Fix beta label width. 2026-02-11 18:08:57 -05:00
Michelle Tang
9a04cd9e3b Move education sheet to safety numbers screen. 2026-02-11 18:08:57 -05:00
Cody Henthorne
dd396eb75a Fix group updates from others showing before messages made before the update. 2026-02-11 18:08:57 -05:00
Greyson Parrelli
611b52780e Fix sticker grid sometimes getting stuck on 2 columns. 2026-02-11 18:08:57 -05:00
Michelle Tang
20a05220ea Adjust strings. 2026-02-11 18:08:57 -05:00
Greyson Parrelli
38e8f24c20 Fix gif and stickers buttons disappearing after exiting edit. 2026-02-11 18:08:57 -05:00
Alex Hart
58d2c92102 Move the rest of the permissions classes. 2026-02-11 18:08:57 -05:00
Greyson Parrelli
f90ba45940 Make pinned chat limit remote configurable. 2026-02-11 18:08:57 -05:00
Tom Ritter
1ecf42bfd3 Fix the message edited label not always appearing.
Closes signalapp/Signal-Android#14582
2026-02-11 18:08:57 -05:00
Greyson Parrelli
ed56c21e5b Fix bug where image wasn't showing after send. 2026-02-11 18:08:57 -05:00
Greyson Parrelli
a0c55baf39 Show whose safety number changed in group chat summaries. 2026-02-10 11:34:24 -05:00
Greyson Parrelli
3b3ef0d545 Allow some context menu actions in search. 2026-02-10 11:30:05 -05:00
jeffrey-signal
78e7f99344 Add group member labels to conversation items. 2026-02-10 10:35:54 -05:00
jeffrey-signal
d709d67f54 Fix position of clearable text field remaining characters count. 2026-02-10 10:35:54 -05:00
jeffrey-signal
6d30fd11a7 Add description to edit member label screen. 2026-02-10 10:35:54 -05:00
Cody Henthorne
4a39c7950f Split performance benchmark and baseline profile into separate modules. 2026-02-10 10:35:54 -05:00
Greyson Parrelli
3dd5ad2a8a Fix video scrubber contrast with HDR video.
Co-authored-by: Milan Stevanovic <milan@signal.org>
2026-02-10 10:35:54 -05:00
jeffrey-signal
d7b7727aa6 Show member labels in conversation settings. 2026-02-10 10:35:54 -05:00
Cody Henthorne
0199cd24ef Remove proto files from APK. 2026-02-10 10:35:54 -05:00
jeffrey-signal
8513e8c4f9 Fix donation learn more sheet icon colors. 2026-02-10 10:35:54 -05:00
Greyson Parrelli
5d2d9017f1 Fix for some HDR transcoding issues.
Co-authored-by: Milan Stevanovic <milan@signal.org>
2026-02-10 10:35:54 -05:00
Alex Hart
879e8f98bd Ensure lint configuration is applied to all modules. 2026-02-10 10:35:54 -05:00
Greyson Parrelli
2c6524f6c0 Include battery info debuglog. 2026-02-10 10:35:54 -05:00
Greyson Parrelli
c9bd81d332 Show linked deviceId's for internal users. 2026-02-10 10:35:54 -05:00
Greyson Parrelli
2d29b02cea Add new sticker packs.
- My Daily Life 2
- Rocky Talk
- Cozy Season
- Chug the Mouse
- Croco's Feelings as blessed sticker packs
2026-02-10 10:35:54 -05:00
andrew-signal
c0a279fcc5 Bump to libsignal v0.87.1 2026-02-10 10:35:54 -05:00
Greyson Parrelli
1a612fab0b Update video demo app to minSdk 26. 2026-02-10 10:35:54 -05:00
Michelle Tang
63e821634a Adjust strings. 2026-02-10 10:35:54 -05:00
Alex Hart
62d951b438 Move additional fragments to core UI. 2026-02-10 10:35:54 -05:00
Greyson Parrelli
8d749c404f Refactor and improve video demo app. 2026-02-10 10:35:54 -05:00
jeffrey-signal
d5b2f4fdd3 Display member label on recipient details sheet. 2026-02-10 10:35:54 -05:00
Alex Hart
fae4ca91bd Ensure spans are accounted for when measuring text. 2026-02-10 10:35:54 -05:00
Alex Hart
6e92ff5096 Move fragments to core UI. 2026-02-10 10:35:54 -05:00
jeffrey-signal
61522cd682 Fix GroupTable.memberLabel() to return null for blank labels. 2026-02-10 10:35:54 -05:00
jeffrey-signal
74dbd0814a Add comment to clarify intentional fall-through in DeviceTransferSetUpFragment switch statement.
Resolves signalapp/Signal-Android##14437
2026-02-10 10:35:54 -05:00
Alex Hart
05751a5b79 Remove app module SignalTheme. 2026-02-10 10:35:54 -05:00
Alex Hart
7741844055 Add audio-indicator slot to use for single-remote-participant calls. 2026-02-10 10:35:54 -05:00
Cody Henthorne
09c07f0707 Fix benchmark tests. 2026-02-10 10:35:54 -05:00
Alex Hart
c565db812e Fix back button intermittently not working in conversation view.
Co-authored-by: Greyson Parrelli <greyson@signal.org>
2026-02-10 10:35:54 -05:00
Greyson Parrelli
00b72c9263 Clean up system for blessed sticker packs. 2026-02-10 10:35:54 -05:00
Alex Hart
5c415139fd Reshape entry point for V3 media screens. 2026-02-10 10:35:54 -05:00
jeffrey-signal
6d944c0f8c Add tests for MemberLabelViewModel. 2026-02-10 10:35:53 -05:00
Michelle Tang
6744a79325 Avoid showing KT for note to self. 2026-02-10 10:35:53 -05:00
Michelle Tang
472d86acc0 Bump version to 7.73.2 2026-02-09 16:33:14 -05:00
Michelle Tang
c1ee7a53b5 Update translations and other static files. 2026-02-09 16:29:47 -05:00
Greyson Parrelli
8d44640377 Prompt for microphone permission when recording video in new camera.
Previously, the new camera would silently record video without audio
when microphone permission was missing. Now it shows the same rationale
dialog and permanent denial flow as the old camera.
2026-02-09 15:52:03 -05:00
Greyson Parrelli
cbcbe3f564 Use OrientationEventListener to update CameraX target rotation. 2026-02-09 14:15:52 -05:00
Greyson Parrelli
698923423f Downsample gallery thumbnail to display size in camera HUD.
Was loading full size image, potentially causing OOM.
2026-02-09 12:01:40 -05:00
1045 changed files with 116994 additions and 43996 deletions

View File

@@ -1,17 +1,7 @@
@file:Suppress("UnstableApiUsage")
import com.android.build.api.dsl.ManagedVirtualDevice
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.provider.ValueSource
import org.gradle.api.provider.ValueSourceParameters
import org.gradle.api.tasks.InputDirectory
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.Optional
import org.gradle.api.tasks.PathSensitive
import org.gradle.api.tasks.PathSensitivity
import org.gradle.api.tasks.testing.logging.TestExceptionFormat
import java.io.File
import java.util.Properties
plugins {
@@ -20,6 +10,7 @@ plugins {
alias(libs.plugins.ktlint)
alias(libs.plugins.compose.compiler)
alias(libs.plugins.kotlinx.serialization)
alias(benchmarkLibs.plugins.baselineprofile)
id("androidx.navigation.safeargs")
id("kotlin-parcelize")
id("com.squareup.wire")
@@ -29,8 +20,8 @@ plugins {
apply(from = "static-ips.gradle.kts")
val canonicalVersionCode = 1646
val canonicalVersionName = "7.73.1"
val canonicalVersionCode = 1655
val canonicalVersionName = "8.0.3"
val currentHotfixVersion = 0
val maxHotfixVersions = 100
@@ -49,6 +40,14 @@ val languagesForBuildConfigProvider = languagesProvider.map { languages ->
languages.joinToString(separator = ", ") { language -> "\"$language\"" }
}
val localPropertiesFile = File(rootProject.projectDir, "local.properties")
val localProperties: Properties? = if (localPropertiesFile.exists()) {
Properties().apply { localPropertiesFile.inputStream().use { load(it) } }
} else {
null
}
val quickstartCredentialsDir: String? = localProperties?.getProperty("quickstart.credentials.dir")
val selectableVariants = listOf(
"nightlyBackupRelease",
"nightlyBackupSpinner",
@@ -60,6 +59,8 @@ val selectableVariants = listOf(
"playProdSpinner",
"playProdCanary",
"playProdPerf",
"playProdMocked",
"playProdNonMinifiedMocked",
"playProdBenchmark",
"playProdInstrumentation",
"playProdRelease",
@@ -69,6 +70,8 @@ val selectableVariants = listOf(
"playStagingPerf",
"playStagingInstrumentation",
"playStagingRelease",
"playProdQuickstart",
"playStagingQuickstart",
"websiteProdSpinner",
"websiteProdRelease",
"githubProdSpinner",
@@ -176,7 +179,8 @@ android {
"META-INF/LICENSE-notice.md",
"META-INF/proguard/androidx-annotations.pro",
"**/*.dylib",
"**/*.dll"
"**/*.dll",
"**/*.proto"
)
}
}
@@ -255,7 +259,7 @@ android {
buildConfigField("String", "STRIPE_PUBLISHABLE_KEY", "\"pk_live_6cmGZopuTsV8novGgJJW9JpC00vLIgtQ1D\"")
buildConfigField("boolean", "TRACING_ENABLED", "false")
buildConfigField("boolean", "LINK_DEVICE_UX_ENABLED", "false")
buildConfigField("boolean", "USE_STRING_ID", "true")
buildConfigField("boolean", "USE_STRING_ID", "false")
ndk {
abiFilters += listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
@@ -354,8 +358,25 @@ android {
isDebuggable = false
isMinifyEnabled = true
matchingFallbacks += "debug"
applicationIdSuffix = ".benchmark"
buildConfigField("String", "BUILD_VARIANT_TYPE", "\"Benchmark\"")
buildConfigField("boolean", "TRACING_ENABLED", "true")
buildConfigField("String[]", "UNIDENTIFIED_SENDER_TRUST_ROOTS", "new String[]{ \"BVT/2gHqbrG1xzuIypLIOjFgMtihrMld1/5TGADL6Dhv\"}")
manifestPlaceholders["applicationClass"] = "org.thoughtcrime.securesms.BenchmarkApplicationContext"
}
create("mocked") {
initWith(getByName("debug"))
isDefault = false
isDebuggable = false
isMinifyEnabled = true
matchingFallbacks += "debug"
buildConfigField("String", "BUILD_VARIANT_TYPE", "\"Benchmark\"")
buildConfigField("boolean", "TRACING_ENABLED", "true")
manifestPlaceholders["applicationClass"] = "org.thoughtcrime.securesms.ApplicationContext"
}
create("canary") {
@@ -365,6 +386,14 @@ android {
matchingFallbacks += "debug"
buildConfigField("String", "BUILD_VARIANT_TYPE", "\"Canary\"")
}
create("quickstart") {
initWith(getByName("debug"))
isDefault = false
isMinifyEnabled = false
matchingFallbacks += "debug"
buildConfigField("String", "BUILD_VARIANT_TYPE", "\"Quickstart\"")
}
}
productFlavors {
@@ -431,7 +460,6 @@ android {
buildConfigField("String", "RECAPTCHA_PROOF_URL", "\"https://signalcaptchas.org/staging/challenge/generate.html\"")
buildConfigField("org.signal.libsignal.net.Network.Environment", "LIBSIGNAL_NET_ENV", "org.signal.libsignal.net.Network.Environment.STAGING")
buildConfigField("int", "LIBSIGNAL_LOG_LEVEL", "org.signal.libsignal.protocol.logging.SignalProtocolLogger.DEBUG")
buildConfigField("boolean", "USE_STRING_ID", "false")
buildConfigField("String", "BUILD_ENVIRONMENT_TYPE", "\"Staging\"")
buildConfigField("String", "STRIPE_PUBLISHABLE_KEY", "\"pk_test_sngOd8FnXNkpce9nPXawKrJD00kIDngZkD\"")
@@ -456,6 +484,7 @@ android {
ignoreWarnings = true
quiet = true
disable += "LintError"
lintConfig = rootProject.file("lint.xml")
}
androidComponents {
@@ -493,6 +522,24 @@ android {
}
}
}
onVariants(selector().withBuildType("quickstart")) { variant ->
val environment = variant.flavorName?.let { name ->
when {
name.contains("staging", ignoreCase = true) -> "staging"
name.contains("prod", ignoreCase = true) -> "prod"
else -> "prod"
}
} ?: "prod"
val taskProvider = tasks.register<CopyQuickstartCredentialsTask>("copyQuickstartCredentials${variant.name.capitalize()}") {
if (quickstartCredentialsDir != null) {
inputDir.set(File(quickstartCredentialsDir))
}
filePrefix.set("${environment}_")
}
variant.sources.assets?.addGeneratedSourceDirectory(taskProvider) { it.outputDir }
}
}
val releaseDir = "$projectDir/src/release/java"
@@ -505,6 +552,18 @@ android {
}
}
sourceSets {
getByName("mocked") {
java.srcDir("$projectDir/src/benchmarkShared/java")
manifest.srcFile("$projectDir/src/benchmarkShared/AndroidManifest.xml")
}
getByName("benchmark") {
java.srcDir("$projectDir/src/benchmarkShared/java")
manifest.srcFile("$projectDir/src/benchmarkShared/AndroidManifest.xml")
}
}
applicationVariants.configureEach {
outputs.configureEach {
if (this is com.android.build.gradle.internal.api.BaseVariantOutputImpl) {
@@ -514,6 +573,20 @@ android {
}
}
baselineProfile {
warnings {
disabledVariants = false
}
mergeIntoMain = true
variants.create("mocked") {
from(project(":baseline-profile"))
}
dexLayoutOptimization = false
}
dependencies {
lintChecks(project(":lintchecks"))
ktlintRuleset(libs.ktlint.twitter.compose)
@@ -537,6 +610,7 @@ dependencies {
implementation(project(":core:models"))
implementation(project(":core:models-jvm"))
implementation(project(":feature:camera"))
implementation(project(":feature:registration"))
implementation(libs.androidx.fragment.ktx)
implementation(libs.androidx.appcompat) {
@@ -791,3 +865,38 @@ abstract class PropertiesFileValueSource : ValueSource<Properties?, PropertiesFi
fun String.capitalize(): String {
return this.replaceFirstChar { it.uppercase() }
}
abstract class CopyQuickstartCredentialsTask : DefaultTask() {
@get:InputDirectory
@get:Optional
abstract val inputDir: DirectoryProperty
@get:Input
abstract val filePrefix: Property<String>
@get:OutputDirectory
abstract val outputDir: DirectoryProperty
@TaskAction
fun copy() {
if (!inputDir.isPresent) {
throw GradleException("quickstart.credentials.dir is not set in local.properties. This is required for quickstart builds.")
}
val prefix = filePrefix.get()
val candidates = inputDir.get().asFile.listFiles()
?.filter { it.extension == "json" && it.name.startsWith(prefix) }
?: emptyList()
if (candidates.isEmpty()) {
throw GradleException("No credential files matching '$prefix*.json' found in ${inputDir.get().asFile}. Add files like '${prefix}account1.json' to your credentials directory.")
}
val chosen = candidates.random()
logger.lifecycle("Selected quickstart credential: ${chosen.name}")
val dest = outputDir.get().asFile.resolve("quickstart")
dest.mkdirs()
chosen.copyTo(dest.resolve(chosen.name), overwrite = true)
}
}

View File

@@ -18,7 +18,6 @@ import com.bumptech.glide.RequestManager
import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test
import org.signal.ringrtc.CallLinkEpoch
import org.signal.ringrtc.CallLinkRootKey
import org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackState
import org.thoughtcrime.securesms.contactshare.Contact
@@ -329,7 +328,7 @@ class V2ConversationItemShapeTest {
override fun onShowGroupDescriptionClicked(groupName: String, description: String, shouldLinkifyWebLinks: Boolean) = Unit
override fun onJoinCallLink(callLinkRootKey: CallLinkRootKey, callLinkEpoch: CallLinkEpoch?) = Unit
override fun onJoinCallLink(callLinkRootKey: CallLinkRootKey) = Unit
override fun onItemClick(item: MultiselectPart?) = Unit

View File

@@ -6,9 +6,9 @@
package org.thoughtcrime.securesms.database
import org.signal.core.util.Base64
import org.signal.core.util.Util
import org.thoughtcrime.securesms.attachments.AttachmentId
import org.thoughtcrime.securesms.attachments.Cdn
import org.thoughtcrime.securesms.util.Util
import org.whispersystems.signalservice.api.attachment.AttachmentUploadResult
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId
import kotlin.random.Random

View File

@@ -14,6 +14,7 @@ import org.junit.runner.RunWith
import org.signal.core.models.ServiceId
import org.signal.core.models.media.TransformProperties
import org.signal.core.util.Base64
import org.signal.core.util.Util
import org.signal.core.util.readFully
import org.signal.core.util.stream.LimitedInputStream
import org.signal.core.util.update
@@ -28,7 +29,6 @@ import org.thoughtcrime.securesms.mms.SentMediaQuality
import org.thoughtcrime.securesms.providers.BlobProvider
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.MediaUtil
import org.thoughtcrime.securesms.util.Util
import org.whispersystems.signalservice.internal.crypto.PaddingInputStream
import java.io.File
import java.util.UUID

View File

@@ -81,8 +81,7 @@ class CallLinkTableTest {
roomId = CallLinkRoomId.fromBytes(roomId),
credentials = CallLinkCredentials(
linkKeyBytes = roomId,
adminPassBytes = null,
epochBytes = null
adminPassBytes = null
),
state = SignalCallLinkState(),
deletionTimestamp = 0L

View File

@@ -18,6 +18,7 @@ import org.signal.core.models.ServiceId.ACI
import org.signal.core.models.ServiceId.PNI
import org.signal.core.util.Base64
import org.signal.core.util.SqlUtil
import org.signal.core.util.Util
import org.signal.core.util.exists
import org.signal.core.util.orNull
import org.signal.core.util.readToSingleBoolean
@@ -43,7 +44,6 @@ import org.thoughtcrime.securesms.mms.IncomingMessage
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.util.Util
import java.util.Optional
import java.util.UUID

View File

@@ -21,6 +21,7 @@ import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.signal.core.util.Base64
import org.signal.core.util.Util
import org.signal.core.util.logging.Log
import org.signal.core.util.update
import org.signal.core.util.withinTransaction
@@ -36,7 +37,6 @@ import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.testing.MessageContentFuzzer.DeleteForMeSync
import org.thoughtcrime.securesms.testing.SignalActivityRule
import org.thoughtcrime.securesms.util.IdentityUtil
import org.thoughtcrime.securesms.util.Util
import org.whispersystems.signalservice.api.attachment.AttachmentUploadResult
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId
import java.util.UUID

View File

@@ -17,11 +17,13 @@ import kotlin.random.Random
* Helper methods for creating groups for message processing tests et al.
*/
object GroupTestingUtils {
fun member(aci: ACI, revision: Int = 0, role: Member.Role = Member.Role.ADMINISTRATOR): DecryptedMember {
fun member(aci: ACI, revision: Int = 0, role: Member.Role = Member.Role.ADMINISTRATOR, labelEmoji: String = "", labelString: String = ""): DecryptedMember {
return DecryptedMember.Builder()
.aciBytes(aci.toByteString())
.joinedAtRevision(revision)
.role(role)
.labelEmoji(labelEmoji)
.labelString(labelString)
.build()
}

View File

@@ -11,6 +11,7 @@ import androidx.test.platform.app.InstrumentationRegistry
import kotlinx.coroutines.runBlocking
import org.junit.rules.ExternalResource
import org.signal.core.models.ServiceId.ACI
import org.signal.core.util.Util
import org.signal.libsignal.protocol.IdentityKey
import org.signal.libsignal.protocol.IdentityKeyPair
import org.signal.libsignal.protocol.SignalProtocolAddress
@@ -32,7 +33,6 @@ import org.thoughtcrime.securesms.registration.data.RegistrationData
import org.thoughtcrime.securesms.registration.data.RegistrationRepository
import org.thoughtcrime.securesms.registration.util.RegistrationUtil
import org.thoughtcrime.securesms.testing.GroupTestingUtils.asMember
import org.thoughtcrime.securesms.util.Util
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile
import org.whispersystems.signalservice.api.push.SignalServiceAddress
import java.util.UUID

View File

@@ -1,16 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<profileable android:shell="true" />
<activity android:name="org.signal.benchmark.BenchmarkSetupActivity"
android:launchMode="singleTask"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:windowSoftInputMode="stateHidden"
android:exported="true"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
</application>
</manifest>

View File

@@ -0,0 +1,167 @@
/*
* Copyright 2026 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms
import android.app.Application
import org.signal.libsignal.net.Network
import org.thoughtcrime.securesms.database.JobDatabase
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.dependencies.ApplicationDependencyProvider
import org.thoughtcrime.securesms.jobmanager.Job
import org.thoughtcrime.securesms.jobmanager.JobManager
import org.thoughtcrime.securesms.jobmanager.JobMigrator
import org.thoughtcrime.securesms.jobmanager.impl.FactoryJobPredicate
import org.thoughtcrime.securesms.jobs.AccountConsistencyWorkerJob
import org.thoughtcrime.securesms.jobs.ArchiveBackupIdReservationJob
import org.thoughtcrime.securesms.jobs.AttachmentCompressionJob
import org.thoughtcrime.securesms.jobs.AttachmentUploadJob
import org.thoughtcrime.securesms.jobs.CreateReleaseChannelJob
import org.thoughtcrime.securesms.jobs.DirectoryRefreshJob
import org.thoughtcrime.securesms.jobs.DownloadLatestEmojiDataJob
import org.thoughtcrime.securesms.jobs.EmojiSearchIndexDownloadJob
import org.thoughtcrime.securesms.jobs.FastJobStorage
import org.thoughtcrime.securesms.jobs.FontDownloaderJob
import org.thoughtcrime.securesms.jobs.GroupCallUpdateSendJob
import org.thoughtcrime.securesms.jobs.GroupRingCleanupJob
import org.thoughtcrime.securesms.jobs.GroupV2UpdateSelfProfileKeyJob
import org.thoughtcrime.securesms.jobs.IndividualSendJob
import org.thoughtcrime.securesms.jobs.JobManagerFactories
import org.thoughtcrime.securesms.jobs.LinkedDeviceInactiveCheckJob
import org.thoughtcrime.securesms.jobs.MarkerJob
import org.thoughtcrime.securesms.jobs.MultiDeviceProfileKeyUpdateJob
import org.thoughtcrime.securesms.jobs.PostRegistrationBackupRedemptionJob
import org.thoughtcrime.securesms.jobs.PreKeysSyncJob
import org.thoughtcrime.securesms.jobs.ProfileUploadJob
import org.thoughtcrime.securesms.jobs.PushGroupSendJob
import org.thoughtcrime.securesms.jobs.PushProcessMessageJob
import org.thoughtcrime.securesms.jobs.ReactionSendJob
import org.thoughtcrime.securesms.jobs.RefreshAttributesJob
import org.thoughtcrime.securesms.jobs.RetrieveRemoteAnnouncementsJob
import org.thoughtcrime.securesms.jobs.RotateCertificateJob
import org.thoughtcrime.securesms.jobs.SendDeliveryReceiptJob
import org.thoughtcrime.securesms.jobs.StickerPackDownloadJob
import org.thoughtcrime.securesms.jobs.StorageSyncJob
import org.thoughtcrime.securesms.jobs.StoryOnboardingDownloadJob
import org.thoughtcrime.securesms.jobs.TypingSendJob
import org.thoughtcrime.securesms.net.DeviceTransferBlockingInterceptor
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.whispersystems.signalservice.api.util.UptimeSleepTimer
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
class BenchmarkApplicationContext : ApplicationContext() {
override fun initializeAppDependencies() {
AppDependencies.init(this, BenchmarkDependencyProvider(this, ApplicationDependencyProvider(this)))
DeviceTransferBlockingInterceptor.getInstance().blockNetwork()
}
override fun onForeground() = Unit
class BenchmarkDependencyProvider(val application: Application, private val default: ApplicationDependencyProvider) : AppDependencies.Provider by default {
override fun provideAuthWebSocket(
signalServiceConfigurationSupplier: Supplier<SignalServiceConfiguration>,
libSignalNetworkSupplier: Supplier<Network>
): SignalWebSocket.AuthenticatedWebSocket {
return SignalWebSocket.AuthenticatedWebSocket(
connectionFactory = { BenchmarkWebSocketConnection.createAuthInstance() },
canConnect = { true },
sleepTimer = UptimeSleepTimer(),
disconnectTimeoutMs = 15.seconds.inWholeMilliseconds
)
}
override fun provideUnauthWebSocket(
signalServiceConfigurationSupplier: Supplier<SignalServiceConfiguration>,
libSignalNetworkSupplier: Supplier<Network>
): SignalWebSocket.UnauthenticatedWebSocket {
return SignalWebSocket.UnauthenticatedWebSocket(
connectionFactory = { BenchmarkWebSocketConnection.createUnauthInstance() },
canConnect = { true },
sleepTimer = UptimeSleepTimer(),
disconnectTimeoutMs = 15.seconds.inWholeMilliseconds
)
}
override fun provideJobManager(): JobManager {
val config = JobManager.Configuration.Builder()
.setJobFactories(filterJobFactories(JobManagerFactories.getJobFactories(application)))
.setConstraintFactories(JobManagerFactories.getConstraintFactories(application))
.setConstraintObservers(JobManagerFactories.getConstraintObservers(application))
.setJobStorage(FastJobStorage(JobDatabase.getInstance(application)))
.setJobMigrator(JobMigrator(TextSecurePreferences.getJobManagerVersion(application), JobManager.CURRENT_VERSION, JobManagerFactories.getJobMigrations(application)))
.addReservedJobRunner(FactoryJobPredicate(PushProcessMessageJob.KEY, MarkerJob.KEY))
.addReservedJobRunner(FactoryJobPredicate(AttachmentUploadJob.KEY, AttachmentCompressionJob.KEY))
.addReservedJobRunner(
FactoryJobPredicate(
IndividualSendJob.KEY,
PushGroupSendJob.KEY,
ReactionSendJob.KEY,
TypingSendJob.KEY,
GroupCallUpdateSendJob.KEY,
SendDeliveryReceiptJob.KEY
)
)
.build()
return JobManager(application, config)
}
private fun filterJobFactories(jobFactories: Map<String, Job.Factory<*>>): Map<String, Job.Factory<*>> {
val blockedJobs = setOf(
AccountConsistencyWorkerJob.KEY,
ArchiveBackupIdReservationJob.KEY,
CreateReleaseChannelJob.KEY,
DirectoryRefreshJob.KEY,
DownloadLatestEmojiDataJob.KEY,
EmojiSearchIndexDownloadJob.KEY,
FontDownloaderJob.KEY,
GroupRingCleanupJob.KEY,
GroupV2UpdateSelfProfileKeyJob.KEY,
LinkedDeviceInactiveCheckJob.KEY,
MultiDeviceProfileKeyUpdateJob.KEY,
PostRegistrationBackupRedemptionJob.KEY,
PreKeysSyncJob.KEY,
ProfileUploadJob.KEY,
RefreshAttributesJob.KEY,
RetrieveRemoteAnnouncementsJob.KEY,
RotateCertificateJob.KEY,
StickerPackDownloadJob.KEY,
StorageSyncJob.KEY,
StoryOnboardingDownloadJob.KEY
)
return jobFactories.mapValues {
if (it.key in blockedJobs) {
NoOpJob.Factory()
} else {
it.value
}
}
}
}
private class NoOpJob(parameters: Parameters) : Job(parameters) {
companion object {
const val KEY = "NoOpJob"
}
override fun serialize(): ByteArray? = null
override fun getFactoryKey(): String = KEY
override fun run(): Result = Result.success()
override fun onFailure() = Unit
class Factory : Job.Factory<NoOpJob> {
override fun create(parameters: Parameters, serializedData: ByteArray?): NoOpJob {
return NoOpJob(parameters)
}
}
}
}

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application
android:name="${applicationClass}"
tools:replace="name">
<profileable android:shell="true" />
<activity
android:name="org.signal.benchmark.BenchmarkSetupActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:exported="true"
android:launchMode="singleTask"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:windowSoftInputMode="stateHidden" />
<receiver
android:name="org.signal.benchmark.BenchmarkCommandReceiver"
android:exported="true">
<intent-filter>
<action android:name="org.signal.benchmark.action.COMMAND" />
</intent-filter>
</receiver>
</application>
</manifest>

View File

@@ -0,0 +1,125 @@
/*
* Copyright 2026 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.benchmark
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.signal.benchmark.setup.Generator
import org.signal.benchmark.setup.Harness
import org.signal.core.util.ThreadUtil
import org.signal.core.util.logging.Log
import org.whispersystems.signalservice.internal.push.Envelope
import org.whispersystems.signalservice.internal.websocket.BenchmarkWebSocketConnection
import org.whispersystems.signalservice.internal.websocket.WebSocketRequestMessage
import kotlin.random.Random
/**
* A BroadcastReceiver that accepts commands sent from the benchmark app to perform
* background operations on the client.
*/
class BenchmarkCommandReceiver : BroadcastReceiver() {
companion object {
private val TAG = Log.tag(BenchmarkCommandReceiver::class)
const val ACTION_COMMAND = "org.signal.benchmark.action.COMMAND"
const val EXTRA_COMMAND = "command"
}
override fun onReceive(context: Context, intent: Intent) {
if (intent.action != ACTION_COMMAND) {
Log.w(TAG, "Ignoring unknown action: ${intent.action}")
return
}
val command = intent.getStringExtra(EXTRA_COMMAND)
Log.i(TAG, "Received command: $command")
when (command) {
"individual-send" -> handlePrepareIndividualSend()
"group-send" -> handlePrepareGroupSend()
"release-messages" -> {
BenchmarkWebSocketConnection.authInstance.startWholeBatchTrace = true
BenchmarkWebSocketConnection.authInstance.releaseMessages()
}
else -> Log.w(TAG, "Unknown command: $command")
}
}
private fun handlePrepareIndividualSend() {
val client = Harness.otherClients[0]
// Send message from Bob to Self
val encryptedEnvelope = client.encrypt(Generator.encryptedTextMessage(System.currentTimeMillis()))
runBlocking {
launch(Dispatchers.IO) {
BenchmarkWebSocketConnection.authInstance.run {
Log.i(TAG, "Sending initial message form Bob to establish session.")
addPendingMessages(listOf(encryptedEnvelope.toWebSocketPayload()))
releaseMessages()
// Sleep briefly to let the message be processed.
ThreadUtil.sleep(100)
}
}
}
// Have Bob generate N messages that will be received by Alice
val messageCount = 100
val envelopes = client.generateInboundEnvelopes(messageCount)
val messages = envelopes.map { e -> e.toWebSocketPayload() }
BenchmarkWebSocketConnection.authInstance.addPendingMessages(messages)
BenchmarkWebSocketConnection.authInstance.addQueueEmptyMessage()
}
private fun handlePrepareGroupSend() {
val clients = Harness.otherClients.take(5)
// Send message from others to Self in the group
val encryptedEnvelopes = clients.map { it.encrypt(Generator.encryptedTextMessage(System.currentTimeMillis(), groupMasterKey = Harness.groupMasterKey)) }
runBlocking {
launch(Dispatchers.IO) {
BenchmarkWebSocketConnection.authInstance.run {
Log.i(TAG, "Sending initial group messages from client to establish sessions.")
addPendingMessages(encryptedEnvelopes.map { it.toWebSocketPayload() })
releaseMessages()
// Sleep briefly to let the messages be processed.
ThreadUtil.sleep(1000)
}
}
}
// Have clients generate N group messages that will be received by Alice
clients.forEach { client ->
val messageCount = 100
val envelopes = client.generateInboundGroupEnvelopes(messageCount, Harness.groupMasterKey)
val messages = envelopes.map { e -> e.toWebSocketPayload() }
BenchmarkWebSocketConnection.authInstance.addPendingMessages(messages)
}
BenchmarkWebSocketConnection.authInstance.addQueueEmptyMessage()
}
private fun Envelope.toWebSocketPayload(): WebSocketRequestMessage {
return WebSocketRequestMessage(
verb = "PUT",
path = "/api/v1/message",
id = Random.nextLong(),
headers = listOf("X-Signal-Timestamp: ${this.timestamp}"),
body = this.encodeByteString()
)
}
}

View File

@@ -15,6 +15,8 @@ class BenchmarkSetupActivity : BaseActivity() {
when (intent.extras!!.getString("setup-type")) {
"cold-start" -> setupColdStart()
"conversation-open" -> setupConversationOpen()
"message-send" -> setupMessageSend()
"group-message-send" -> setupGroupMessageSend()
}
val textView: TextView = TextView(this).apply {
@@ -32,6 +34,8 @@ class BenchmarkSetupActivity : BaseActivity() {
TestMessages.insertIncomingImageMessage(other = recipient, attachmentCount = 1)
TestMessages.insertIncomingImageMessage(other = recipient, attachmentCount = 2, body = "Album")
TestMessages.insertIncomingImageMessage(other = recipient, body = "Test", attachmentCount = 1, failed = true)
TestMessages.insertIncomingTextMessage(other = recipient, body = "Signal message")
TestMessages.insertIncomingTextMessage(other = recipient, body = "Test")
SignalDatabase.messages.setAllMessagesRead()
@@ -54,4 +58,14 @@ class BenchmarkSetupActivity : BaseActivity() {
SignalDatabase.threads.update(SignalDatabase.threads.getOrCreateThreadIdFor(recipient = recipient), true)
}
}
private fun setupMessageSend() {
TestUsers.setupSelf()
TestUsers.setupTestClients(1)
}
private fun setupGroupMessageSend() {
TestUsers.setupSelf()
TestUsers.setupGroup()
}
}

View File

@@ -0,0 +1,64 @@
/*
* Copyright 2026 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.benchmark.setup
import okio.ByteString.Companion.toByteString
import org.signal.core.models.ServiceId
import org.signal.core.util.Base64
import org.signal.core.util.toByteArray
import org.signal.libsignal.zkgroup.groups.GroupMasterKey
import org.thoughtcrime.securesms.messages.SignalServiceProtoUtil.buildWith
import org.whispersystems.signalservice.api.crypto.ContentHint
import org.whispersystems.signalservice.api.crypto.EnvelopeContent
import org.whispersystems.signalservice.internal.push.Content
import org.whispersystems.signalservice.internal.push.DataMessage
import org.whispersystems.signalservice.internal.push.Envelope
import org.whispersystems.signalservice.internal.push.GroupContextV2
import org.whispersystems.signalservice.internal.push.OutgoingPushMessage
import java.util.Optional
import java.util.UUID
object Generator {
fun encryptedTextMessage(
now: Long,
message: String = "Test message",
groupMasterKey: GroupMasterKey? = null
): EnvelopeContent {
val content = Content.Builder().apply {
dataMessage(
DataMessage.Builder().buildWith {
body = message
timestamp = now
if (groupMasterKey != null) {
groupV2 = GroupContextV2.Builder().buildWith {
masterKey = groupMasterKey.serialize().toByteString()
revision = 1
}
}
}
)
}
return EnvelopeContent.encrypted(content.build(), ContentHint.RESENDABLE, Optional.empty())
}
fun OutgoingPushMessage.toEnvelope(timestamp: Long, destination: ServiceId): Envelope {
val serverGuid = UUID.randomUUID()
return Envelope.Builder()
.type(Envelope.Type.fromValue(this.type))
.sourceDevice(1)
.timestamp(timestamp)
.serverTimestamp(timestamp + 1)
.destinationServiceId(destination.toString())
.destinationServiceIdBinary(destination.toByteString())
.serverGuid(serverGuid.toString())
.serverGuidBinary(serverGuid.toByteArray().toByteString())
.content(Base64.decode(this.content).toByteString())
.urgent(true)
.story(false)
.build()
}
}

View File

@@ -0,0 +1,58 @@
/*
* Copyright 2026 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.benchmark.setup
import org.signal.core.models.ServiceId.ACI
import org.signal.core.util.Base64
import org.signal.core.util.Hex
import org.signal.core.util.UuidUtil
import org.signal.libsignal.metadata.certificate.SenderCertificate
import org.signal.libsignal.metadata.certificate.ServerCertificate
import org.signal.libsignal.protocol.IdentityKeyPair
import org.signal.libsignal.protocol.ecc.ECKeyPair
import org.signal.libsignal.protocol.ecc.ECPrivateKey
import org.signal.libsignal.protocol.ecc.ECPublicKey
import org.signal.libsignal.zkgroup.groups.GroupMasterKey
import org.signal.libsignal.zkgroup.profiles.ProfileKey
import java.util.Optional
import java.util.UUID
import kotlin.random.Random
import kotlin.time.Duration
object Harness {
const val SELF_E164 = "+15555559999"
val SELF_ACI = ACI.from(UuidUtil.parseOrThrow("d81b9a54-0ec9-43aa-a73f-7e99280ad53e"))
private val OTHERS_IDENTITY_KEY = IdentityKeyPair(Base64.decode("CiEFbAw403SCGPB+tjqfk+jrH7r9ma1P2hcujqydHRYVzzISIGiWYdWYBBdBzDdF06wgEm+HKcc6ETuWB7Jnvk7Wjw1u"))
private val OTHERS_PROFILE_KEY = ProfileKey(Base64.decode("aJJ/A7GBCSnU9HJ1DdMWcKMMeXQKRUguTlAbtlfo/ik"))
val groupMasterKey = GroupMasterKey(Hex.fromStringCondensed("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"))
val trustRoot = ECKeyPair(
ECPublicKey(Base64.decode("BVT/2gHqbrG1xzuIypLIOjFgMtihrMld1/5TGADL6Dhv")),
ECPrivateKey(Base64.decode("2B1zU7JQdPol/XWiom4pQXrSrHFeO8jzZ1u7wfrtY3o"))
)
val otherClients: List<OtherClient> by lazy {
val random = Random(4242)
buildList {
(0 until 1000).forEach { i ->
val aci = ACI.from(UUID(random.nextLong(), random.nextLong()))
val e164 = "+1555555%04d".format(i)
val identityKey = OTHERS_IDENTITY_KEY
val profileKey = OTHERS_PROFILE_KEY
add(OtherClient(aci, e164, identityKey, profileKey))
}
}
}
fun createCertificateFor(uuid: UUID, e164: String?, deviceId: Int, identityKey: ECPublicKey, expires: Duration): SenderCertificate {
val serverKey: ECKeyPair = ECKeyPair.generate()
val serverCertificate = ServerCertificate(trustRoot.privateKey, 1, serverKey.publicKey)
return serverCertificate.issue(serverKey.privateKey, uuid.toString(), Optional.ofNullable(e164), deviceId, identityKey, expires.inWholeMilliseconds)
}
}

View File

@@ -0,0 +1,189 @@
/*
* Copyright 2026 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.benchmark.setup
import org.signal.benchmark.setup.Generator.toEnvelope
import org.signal.core.models.ServiceId
import org.signal.libsignal.protocol.IdentityKey
import org.signal.libsignal.protocol.IdentityKeyPair
import org.signal.libsignal.protocol.SessionBuilder
import org.signal.libsignal.protocol.SignalProtocolAddress
import org.signal.libsignal.protocol.ecc.ECPublicKey
import org.signal.libsignal.protocol.groups.state.SenderKeyRecord
import org.signal.libsignal.protocol.state.IdentityKeyStore
import org.signal.libsignal.protocol.state.IdentityKeyStore.IdentityChange
import org.signal.libsignal.protocol.state.KyberPreKeyRecord
import org.signal.libsignal.protocol.state.PreKeyBundle
import org.signal.libsignal.protocol.state.PreKeyRecord
import org.signal.libsignal.protocol.state.SessionRecord
import org.signal.libsignal.protocol.state.SignedPreKeyRecord
import org.signal.libsignal.protocol.util.KeyHelper
import org.signal.libsignal.zkgroup.groups.GroupMasterKey
import org.signal.libsignal.zkgroup.profiles.ProfileKey
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.whispersystems.signalservice.api.SignalServiceAccountDataStore
import org.whispersystems.signalservice.api.SignalSessionLock
import org.whispersystems.signalservice.api.crypto.EnvelopeContent
import org.whispersystems.signalservice.api.crypto.SealedSenderAccess
import org.whispersystems.signalservice.api.crypto.SignalServiceCipher
import org.whispersystems.signalservice.api.crypto.SignalSessionBuilder
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess
import org.whispersystems.signalservice.api.push.DistributionId
import org.whispersystems.signalservice.api.push.SignalServiceAddress
import org.whispersystems.signalservice.internal.push.Envelope
import java.util.UUID
import java.util.concurrent.locks.ReentrantLock
import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.milliseconds
/**
* This is a "fake" client that can start a session with the running app's user, referred to as Alice in this
* code.
*/
class OtherClient(val serviceId: ServiceId, val e164: String, val identityKeyPair: IdentityKeyPair, val profileKey: ProfileKey) {
private val serviceAddress = SignalServiceAddress(serviceId, e164)
private val registrationId = KeyHelper.generateRegistrationId(false)
private val aciStore = BobSignalServiceAccountDataStore(registrationId, identityKeyPair)
private val senderCertificate = Harness.createCertificateFor(serviceId.rawUuid, e164, 1, identityKeyPair.publicKey.publicKey, System.currentTimeMillis().milliseconds + 30.days)
private val sessionLock = object : SignalSessionLock {
private val lock = ReentrantLock()
override fun acquire(): SignalSessionLock.Lock {
lock.lock()
return SignalSessionLock.Lock { lock.unlock() }
}
}
/** Inspired by SignalServiceMessageSender#getEncryptedMessage */
fun encrypt(envelopeContent: EnvelopeContent): Envelope {
val cipher = SignalServiceCipher(serviceAddress, 1, aciStore, sessionLock, null)
if (!aciStore.containsSession(getAliceProtocolAddress())) {
val sessionBuilder = SignalSessionBuilder(sessionLock, SessionBuilder(aciStore, getAliceProtocolAddress()))
sessionBuilder.process(getAlicePreKeyBundle())
}
return cipher.encrypt(getAliceProtocolAddress(), getAliceUnidentifiedAccess(), envelopeContent)
.toEnvelope(envelopeContent.content.get().dataMessage!!.timestamp!!, getAliceServiceId())
}
fun generateInboundEnvelopes(count: Int): List<Envelope> {
val envelopes = ArrayList<Envelope>(count)
var now = System.currentTimeMillis()
for (i in 0 until count) {
envelopes += encrypt(Generator.encryptedTextMessage(now))
now += 3
}
return envelopes
}
fun generateInboundGroupEnvelopes(count: Int, groupMasterKey: GroupMasterKey): List<Envelope> {
val envelopes = ArrayList<Envelope>(count)
var now = System.currentTimeMillis()
for (i in 0 until count) {
envelopes += encrypt(Generator.encryptedTextMessage(now, groupMasterKey = groupMasterKey))
now += 3
}
return envelopes
}
private fun getAliceServiceId(): ServiceId {
return SignalStore.account.requireAci()
}
private fun getAlicePreKeyBundle(): PreKeyBundle {
val aliceSignedPreKeyRecord = SignalDatabase.signedPreKeys.getAll(getAliceServiceId()).first()
val aliceSignedKyberPreKeyRecord = SignalDatabase.kyberPreKeys.getAllLastResort(getAliceServiceId()).first().record
return PreKeyBundle(
registrationId = SignalStore.account.registrationId,
deviceId = 1,
preKeyId = PreKeyBundle.NULL_PRE_KEY_ID,
preKeyPublic = null,
signedPreKeyId = aliceSignedPreKeyRecord.id,
signedPreKeyPublic = aliceSignedPreKeyRecord.keyPair.publicKey,
signedPreKeySignature = aliceSignedPreKeyRecord.signature,
identityKey = getAlicePublicKey(),
kyberPreKeyId = aliceSignedKyberPreKeyRecord.id,
kyberPreKeyPublic = aliceSignedKyberPreKeyRecord.keyPair.publicKey,
kyberPreKeySignature = aliceSignedKyberPreKeyRecord.signature
)
}
private fun getAliceProtocolAddress(): SignalProtocolAddress {
return SignalProtocolAddress(SignalStore.account.requireAci().toString(), 1)
}
private fun getAlicePublicKey(): IdentityKey {
return SignalStore.account.aciIdentityKey.publicKey
}
private fun getAliceProfileKey(): ProfileKey {
return ProfileKeyUtil.getSelfProfileKey()
}
private fun getAliceUnidentifiedAccess(): SealedSenderAccess? {
val theirProfileKey = getAliceProfileKey()
val themUnidentifiedAccessKey = UnidentifiedAccess(UnidentifiedAccess.deriveAccessKeyFrom(theirProfileKey), senderCertificate.serialized, false)
return SealedSenderAccess.forIndividual(themUnidentifiedAccessKey)
}
private class BobSignalServiceAccountDataStore(private val registrationId: Int, private val identityKeyPair: IdentityKeyPair) : SignalServiceAccountDataStore {
private var aliceSessionRecord: SessionRecord? = null
override fun getIdentityKeyPair(): IdentityKeyPair = identityKeyPair
override fun getLocalRegistrationId(): Int = registrationId
override fun isTrustedIdentity(address: SignalProtocolAddress?, identityKey: IdentityKey?, direction: IdentityKeyStore.Direction?): Boolean = true
override fun loadSession(address: SignalProtocolAddress?): SessionRecord = aliceSessionRecord ?: SessionRecord()
override fun saveIdentity(address: SignalProtocolAddress?, identityKey: IdentityKey?): IdentityChange = IdentityChange.NEW_OR_UNCHANGED
override fun storeSession(address: SignalProtocolAddress?, record: SessionRecord?) {
aliceSessionRecord = record
}
override fun getSubDeviceSessions(name: String?): List<Int> = emptyList()
override fun containsSession(address: SignalProtocolAddress?): Boolean = aliceSessionRecord != null
override fun getIdentity(address: SignalProtocolAddress?): IdentityKey = SignalStore.account.aciIdentityKey.publicKey
override fun loadPreKey(preKeyId: Int): PreKeyRecord = throw UnsupportedOperationException()
override fun storePreKey(preKeyId: Int, record: PreKeyRecord?) = throw UnsupportedOperationException()
override fun containsPreKey(preKeyId: Int): Boolean = throw UnsupportedOperationException()
override fun removePreKey(preKeyId: Int) = throw UnsupportedOperationException()
override fun loadExistingSessions(addresses: MutableList<SignalProtocolAddress>?): MutableList<SessionRecord> = throw UnsupportedOperationException()
override fun deleteSession(address: SignalProtocolAddress?) = throw UnsupportedOperationException()
override fun deleteAllSessions(name: String?) = throw UnsupportedOperationException()
override fun loadSignedPreKey(signedPreKeyId: Int): SignedPreKeyRecord = throw UnsupportedOperationException()
override fun loadSignedPreKeys(): MutableList<SignedPreKeyRecord> = throw UnsupportedOperationException()
override fun storeSignedPreKey(signedPreKeyId: Int, record: SignedPreKeyRecord?) = throw UnsupportedOperationException()
override fun containsSignedPreKey(signedPreKeyId: Int): Boolean = throw UnsupportedOperationException()
override fun removeSignedPreKey(signedPreKeyId: Int) = throw UnsupportedOperationException()
override fun loadKyberPreKey(kyberPreKeyId: Int): KyberPreKeyRecord = throw UnsupportedOperationException()
override fun loadKyberPreKeys(): MutableList<KyberPreKeyRecord> = throw UnsupportedOperationException()
override fun storeKyberPreKey(kyberPreKeyId: Int, record: KyberPreKeyRecord?) = throw UnsupportedOperationException()
override fun containsKyberPreKey(kyberPreKeyId: Int): Boolean = throw UnsupportedOperationException()
override fun markKyberPreKeyUsed(kyberPreKeyId: Int, signedPreKeyId: Int, baseKey: ECPublicKey) = throw UnsupportedOperationException()
override fun deleteAllStaleOneTimeEcPreKeys(threshold: Long, minCount: Int) = throw UnsupportedOperationException()
override fun markAllOneTimeEcPreKeysStaleIfNecessary(staleTime: Long) = throw UnsupportedOperationException()
override fun storeSenderKey(sender: SignalProtocolAddress?, distributionId: UUID?, record: SenderKeyRecord?) = throw UnsupportedOperationException()
override fun loadSenderKey(sender: SignalProtocolAddress?, distributionId: UUID?): SenderKeyRecord = throw UnsupportedOperationException()
override fun archiveSession(address: SignalProtocolAddress?) = throw UnsupportedOperationException()
override fun getAllAddressesWithActiveSessions(addressNames: MutableList<String>?): MutableMap<SignalProtocolAddress, SessionRecord> = throw UnsupportedOperationException()
override fun getSenderKeySharedWith(distributionId: DistributionId?): MutableSet<SignalProtocolAddress> = throw UnsupportedOperationException()
override fun markSenderKeySharedWith(distributionId: DistributionId?, addresses: MutableCollection<SignalProtocolAddress>?) = throw UnsupportedOperationException()
override fun clearSenderKeySharedWith(addresses: MutableCollection<SignalProtocolAddress>?) = throw UnsupportedOperationException()
override fun storeLastResortKyberPreKey(kyberPreKeyId: Int, kyberPreKeyRecord: KyberPreKeyRecord) = throw UnsupportedOperationException()
override fun removeKyberPreKey(kyberPreKeyId: Int) = throw UnsupportedOperationException()
override fun markAllOneTimeKyberPreKeysStaleIfNecessary(staleTime: Long) = throw UnsupportedOperationException()
override fun deleteAllStaleOneTimeKyberPreKeys(threshold: Long, minCount: Int) = throw UnsupportedOperationException()
override fun loadLastResortKyberPreKeys(): List<KyberPreKeyRecord> = throw UnsupportedOperationException()
override fun isMultiDevice(): Boolean = throw UnsupportedOperationException()
}
}

View File

@@ -96,6 +96,7 @@ object TestMessages {
val message = IncomingMessage(
type = MessageType.NORMAL,
from = other.id,
body = body,
sentTimeMillis = timestamp ?: System.currentTimeMillis(),
serverTimeMillis = timestamp ?: System.currentTimeMillis(),
receivedTimeMillis = timestamp ?: System.currentTimeMillis(),

View File

@@ -4,14 +4,24 @@ import android.app.Application
import android.content.SharedPreferences
import android.preference.PreferenceManager
import kotlinx.coroutines.runBlocking
import okio.ByteString
import org.signal.core.models.ServiceId.ACI
import org.signal.core.util.Util
import org.signal.libsignal.protocol.IdentityKeyPair
import org.signal.libsignal.protocol.SignalProtocolAddress
import org.signal.storageservice.storage.protos.groups.AccessControl
import org.signal.storageservice.storage.protos.groups.Member
import org.signal.storageservice.storage.protos.groups.local.DecryptedGroup
import org.signal.storageservice.storage.protos.groups.local.DecryptedMember
import org.signal.storageservice.storage.protos.groups.local.DecryptedTimer
import org.signal.storageservice.storage.protos.groups.local.EnabledState
import org.thoughtcrime.securesms.crypto.MasterSecretUtil
import org.thoughtcrime.securesms.crypto.PreKeyUtil
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.databaseprotos.RestoreDecisionState
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.keyvalue.CertificateType
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.keyvalue.Skipped
import org.thoughtcrime.securesms.net.DeviceTransferBlockingInterceptor
@@ -23,15 +33,16 @@ import org.thoughtcrime.securesms.registration.data.LocalRegistrationMetadataUti
import org.thoughtcrime.securesms.registration.data.RegistrationData
import org.thoughtcrime.securesms.registration.data.RegistrationRepository
import org.thoughtcrime.securesms.registration.util.RegistrationUtil
import org.thoughtcrime.securesms.util.Util
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile
import org.whispersystems.signalservice.api.push.SignalServiceAddress
import java.util.UUID
import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.milliseconds
object TestUsers {
private var generatedOthers: Int = 0
private val TEST_E164 = "+15555550101"
private var generatedOthers: Int = 1
fun setupSelf(): Recipient {
val application: Application = AppDependencies.application
@@ -49,19 +60,19 @@ object TestUsers {
runBlocking {
val registrationData = RegistrationData(
code = "123123",
e164 = TEST_E164,
e164 = Harness.SELF_E164,
password = Util.getSecret(18),
registrationId = RegistrationRepository.getRegistrationId(),
profileKey = RegistrationRepository.getProfileKey(TEST_E164),
profileKey = RegistrationRepository.getProfileKey(Harness.SELF_E164),
fcmToken = null,
pniRegistrationId = RegistrationRepository.getPniRegistrationId(),
recoveryPassword = "asdfasdfasdfasdf"
)
val remoteResult = AccountRegistrationResult(
uuid = UUID.randomUUID().toString(),
uuid = Harness.SELF_ACI.toString(),
pni = UUID.randomUUID().toString(),
storageCapable = false,
number = TEST_E164,
number = Harness.SELF_E164,
masterKey = null,
pin = null,
aciPreKeyCollection = RegistrationRepository.generateSignedAndLastResortPreKeys(SignalStore.account.aciIdentityKey, SignalStore.account.aciPreKeys),
@@ -76,6 +87,31 @@ object TestUsers {
SignalStore.registration.restoreDecisionState = RestoreDecisionState.Skipped
RegistrationUtil.maybeMarkRegistrationComplete()
SignalDatabase.recipients.setProfileName(Recipient.self().id, ProfileName.fromParts("Tester", "McTesterson"))
TextSecurePreferences.setPromptedOptimizeDoze(application, true)
TextSecurePreferences.setRatingEnabled(application, false)
PreKeyUtil.generateAndStoreSignedPreKey(AppDependencies.protocolStore.aci(), SignalStore.account.aciPreKeys)
PreKeyUtil.generateAndStoreOneTimeEcPreKeys(AppDependencies.protocolStore.aci(), SignalStore.account.aciPreKeys)
PreKeyUtil.generateAndStoreOneTimeKyberPreKeys(AppDependencies.protocolStore.aci(), SignalStore.account.aciPreKeys)
val aliceSenderCertificate = Harness.createCertificateFor(
uuid = Harness.SELF_ACI.rawUuid,
e164 = Harness.SELF_E164,
deviceId = 1,
identityKey = SignalStore.account.aciIdentityKey.publicKey.publicKey,
expires = System.currentTimeMillis().milliseconds + 30.days
)
val aliceSenderCertificate2 = Harness.createCertificateFor(
uuid = Harness.SELF_ACI.rawUuid,
e164 = null,
deviceId = 1,
identityKey = SignalStore.account.aciIdentityKey.publicKey.publicKey,
expires = System.currentTimeMillis().milliseconds + 30.days
)
SignalStore.certificate.setUnidentifiedAccessCertificate(CertificateType.ACI_AND_E164, aliceSenderCertificate.serialized)
SignalStore.certificate.setUnidentifiedAccessCertificate(CertificateType.ACI_ONLY, aliceSenderCertificate2.serialized)
return Recipient.self()
}
@@ -110,4 +146,71 @@ object TestUsers {
return others
}
fun setupTestClients(othersCount: Int): List<RecipientId> {
val others = mutableListOf<RecipientId>()
synchronized(this) {
for (i in 0 until othersCount) {
val otherClient = Harness.otherClients[i]
val recipientId = RecipientId.from(SignalServiceAddress(otherClient.serviceId, otherClient.e164))
SignalDatabase.recipients.setProfileName(recipientId, ProfileName.fromParts("Buddy", "#$i"))
SignalDatabase.recipients.setProfileKeyIfAbsent(recipientId, otherClient.profileKey)
SignalDatabase.recipients.setCapabilities(recipientId, SignalServiceProfile.Capabilities(true, true))
SignalDatabase.recipients.setProfileSharing(recipientId, true)
SignalDatabase.recipients.markRegistered(recipientId, otherClient.serviceId)
AppDependencies.protocolStore.aci().saveIdentity(SignalProtocolAddress(otherClient.serviceId.toString(), 1), otherClient.identityKeyPair.publicKey)
others += recipientId
}
generatedOthers += othersCount
}
return others
}
fun setupGroup() {
val members = setupTestClients(5)
val self = Recipient.self()
val fullMembers = buildList {
add(member(aci = self.requireAci()))
addAll(members.map { member(aci = Recipient.resolved(it).requireAci()) })
}
val group = DecryptedGroup(
title = "Title",
avatar = "",
disappearingMessagesTimer = DecryptedTimer(),
accessControl = AccessControl(),
revision = 1,
members = fullMembers,
pendingMembers = emptyList(),
requestingMembers = emptyList(),
inviteLinkPassword = ByteString.EMPTY,
description = "Description",
isAnnouncementGroup = EnabledState.DISABLED,
bannedMembers = emptyList(),
isPlaceholderGroup = false
)
val groupId = SignalDatabase.groups.create(
groupMasterKey = Harness.groupMasterKey,
groupState = group,
groupSendEndorsements = null
)
SignalDatabase.recipients.setProfileSharing(Recipient.externalGroupExact(groupId!!).id, true)
}
private fun member(aci: ACI, role: Member.Role = Member.Role.DEFAULT, joinedAt: Int = 0, labelEmoji: String = "", labelString: String = ""): DecryptedMember {
return DecryptedMember(
role = role,
aciBytes = aci.toByteString(),
joinedAtRevision = joinedAt,
labelEmoji = labelEmoji,
labelString = labelString
)
}
}

View File

@@ -0,0 +1,152 @@
/*
* Copyright 2026 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.signalservice.internal.websocket
import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.subjects.BehaviorSubject
import org.thoughtcrime.securesms.util.JsonUtils
import org.thoughtcrime.securesms.util.SignalTrace
import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState
import org.whispersystems.signalservice.internal.push.SendMessageResponse
import java.net.SocketException
import java.util.LinkedList
import java.util.Optional
import java.util.concurrent.Semaphore
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException
/**
* A [WebSocketConnection] that provides a way to add "incoming" WebSocket payloads
* and have client code pull them off the "wire" as they would a normal socket.
*
* Add messages with [addPendingMessages] and then can release them to the requestor via
* [releaseMessages].
*/
class BenchmarkWebSocketConnection : WebSocketConnection {
companion object {
lateinit var authInstance: BenchmarkWebSocketConnection
private set
@Synchronized
fun createAuthInstance(): WebSocketConnection {
authInstance = BenchmarkWebSocketConnection()
return authInstance
}
lateinit var unauthInstance: BenchmarkWebSocketConnection
private set
@Synchronized
fun createUnauthInstance(): WebSocketConnection {
unauthInstance = BenchmarkWebSocketConnection()
return unauthInstance
}
}
override val name: String = "bench-${System.identityHashCode(this)}"
private val state = BehaviorSubject.create<WebSocketConnectionState>()
private val incomingRequests = LinkedList<WebSocketRequestMessage>()
private val incomingSemaphore = Semaphore(0)
var startWholeBatchTrace = false
@Volatile
private var isShutdown = false
override fun connect(): Observable<WebSocketConnectionState> {
state.onNext(WebSocketConnectionState.CONNECTED)
return state
}
override fun isDead(): Boolean {
return false
}
override fun disconnect() {
state.onNext(WebSocketConnectionState.DISCONNECTED)
// Signal shutdown
isShutdown = true
val queuedThreads = incomingSemaphore.queueLength
if (queuedThreads > 0) {
incomingSemaphore.release(queuedThreads)
}
}
override fun readRequest(timeoutMillis: Long): WebSocketRequestMessage {
if (incomingSemaphore.tryAcquire(1, 10, TimeUnit.SECONDS)) {
// Check if we were woken up due to shutdown
if (isShutdown) {
throw SocketException("WebSocket connection closed")
}
return getNextRequest()
}
throw TimeoutException("Timeout exceeded")
}
override fun readRequestIfAvailable(): Optional<WebSocketRequestMessage> {
return if (incomingSemaphore.tryAcquire()) {
Optional.of(getNextRequest())
} else {
Optional.empty()
}
}
private fun getNextRequest(): WebSocketRequestMessage {
if (startWholeBatchTrace) {
startWholeBatchTrace = false
SignalTrace.beginSection("IncomingMessageObserver#totalProcessing")
}
return incomingRequests.removeFirst()
}
override fun sendResponse(response: WebSocketResponseMessage) = Unit
fun addPendingMessages(messages: List<WebSocketRequestMessage>) {
incomingRequests.addAll(messages)
}
fun releaseMessages() {
incomingSemaphore.release(incomingRequests.size)
}
override fun sendRequest(
request: WebSocketRequestMessage,
timeoutSeconds: Long
): Single<WebsocketResponse> {
if (request.verb != null && request.path != null) {
if (request.verb == "PUT" && request.path!!.startsWith("/v1/messages/")) {
return Single.just(WebsocketResponse(200, SendMessageResponse().toJson(), emptyList<String>(), true))
}
}
return Single.error(okio.IOException("fake timeout"))
}
override fun sendKeepAlive() = Unit
fun addQueueEmptyMessage() {
addPendingMessages(
listOf(
WebSocketRequestMessage(
verb = "PUT",
path = "/api/v1/queue/empty"
)
)
)
}
}
private fun Any.toJson(): String {
return JsonUtils.toJson(this)
}

View File

@@ -14,13 +14,13 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.navigation.fragment.findNavController
import androidx.navigation.navGraphViewModels
import org.signal.core.ui.compose.ComposeFragment
import org.signal.core.ui.compose.NightPreview
import org.signal.core.ui.compose.Previews
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.ComposeFragment
/**
* Configuration fragment for the internal conversation test fragment.

View File

@@ -18,7 +18,6 @@ import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.kotlin.subscribeBy
import org.signal.core.util.concurrent.LifecycleDisposable
import org.signal.core.util.logging.Log
import org.signal.ringrtc.CallLinkEpoch
import org.signal.ringrtc.CallLinkRootKey
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.ViewBinderDelegate
@@ -295,7 +294,7 @@ class InternalConversationTestFragment : Fragment(R.layout.conversation_test_fra
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
}
override fun onJoinCallLink(callLinkRootKey: CallLinkRootKey, callLinkEpoch: CallLinkEpoch?) {
override fun onJoinCallLink(callLinkRootKey: CallLinkRootKey) {
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
}

View File

@@ -475,7 +475,7 @@
android:windowSoftInputMode="stateAlwaysHidden|adjustNothing" />
<activity
android:name=".mediasend.v3.MediaSendV3Activity"
android:name="org.signal.mediasend.MediaSendActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"
android:exported="false"
android:launchMode="singleTop"
@@ -1446,7 +1446,7 @@
android:exported="false" />
<receiver
android:name=".payments.backup.phrase.ClearClipboardAlarmReceiver"
android:name="org.signal.core.util.ClearClipboardAlarmReceiver"
android:exported="false" />
<receiver

File diff suppressed because it is too large Load Diff

View File

@@ -16,7 +16,7 @@ import org.thoughtcrime.securesms.migrations.ApplicationMigrations;
import org.thoughtcrime.securesms.migrations.QuoteThumbnailBackfillMigrationJob;
import org.thoughtcrime.securesms.stickers.BlessedPacks;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.signal.core.util.Util;
/**
* Rule of thumb: if there's something you want to do on the first app launch that involves
@@ -41,11 +41,7 @@ public final class AppInitialization {
TextSecurePreferences.setTypingIndicatorsEnabled(context, true);
AppDependencies.getMegaphoneRepository().onFirstEverAppLaunch();
SignalStore.onFirstEverAppLaunch();
AppDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.ZOZO.getPackId(), BlessedPacks.ZOZO.getPackKey(), false));
AppDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.BANDIT.getPackId(), BlessedPacks.BANDIT.getPackKey(), false));
AppDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.DAY_BY_DAY.getPackId(), BlessedPacks.DAY_BY_DAY.getPackKey(), false));
AppDependencies.getJobManager().add(StickerPackDownloadJob.forReference(BlessedPacks.SWOON_HANDS.getPackId(), BlessedPacks.SWOON_HANDS.getPackKey()));
AppDependencies.getJobManager().add(StickerPackDownloadJob.forReference(BlessedPacks.SWOON_FACES.getPackId(), BlessedPacks.SWOON_FACES.getPackKey()));
AppDependencies.getJobManager().addAll(BlessedPacks.getFirstInstallJobs());
}
public static void onPostBackupRestore(@NonNull Context context) {
@@ -59,11 +55,7 @@ public final class AppInitialization {
SignalStore.notificationProfile().setHasSeenTooltip(true);
TextSecurePreferences.onPostBackupRestore(context);
SignalStore.settings().setPassphraseDisabled(true);
AppDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.ZOZO.getPackId(), BlessedPacks.ZOZO.getPackKey(), false));
AppDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.BANDIT.getPackId(), BlessedPacks.BANDIT.getPackKey(), false));
AppDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.DAY_BY_DAY.getPackId(), BlessedPacks.DAY_BY_DAY.getPackKey(), false));
AppDependencies.getJobManager().add(StickerPackDownloadJob.forReference(BlessedPacks.SWOON_HANDS.getPackId(), BlessedPacks.SWOON_HANDS.getPackKey()));
AppDependencies.getJobManager().add(StickerPackDownloadJob.forReference(BlessedPacks.SWOON_FACES.getPackId(), BlessedPacks.SWOON_FACES.getPackKey()));
AppDependencies.getJobManager().addAll(BlessedPacks.getFirstInstallJobs());
EmojiSearchIndexDownloadJob.scheduleImmediately();
DeleteAbandonedAttachmentsJob.enqueue();
@@ -87,10 +79,6 @@ public final class AppInitialization {
SignalStore.settings().setPassphraseDisabled(true);
AppDependencies.getMegaphoneRepository().onFirstEverAppLaunch();
SignalStore.onFirstEverAppLaunch();
AppDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.ZOZO.getPackId(), BlessedPacks.ZOZO.getPackKey(), false));
AppDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.BANDIT.getPackId(), BlessedPacks.BANDIT.getPackKey(), false));
AppDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.DAY_BY_DAY.getPackId(), BlessedPacks.DAY_BY_DAY.getPackKey(), false));
AppDependencies.getJobManager().add(StickerPackDownloadJob.forReference(BlessedPacks.SWOON_HANDS.getPackId(), BlessedPacks.SWOON_HANDS.getPackKey()));
AppDependencies.getJobManager().add(StickerPackDownloadJob.forReference(BlessedPacks.SWOON_FACES.getPackId(), BlessedPacks.SWOON_FACES.getPackKey()));
AppDependencies.getJobManager().addAll(BlessedPacks.getFirstInstallJobs());
}
}

View File

@@ -104,12 +104,13 @@ import org.thoughtcrime.securesms.service.webrtc.AndroidTelecomUtil;
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
import org.thoughtcrime.securesms.util.AppForegroundObserver;
import org.thoughtcrime.securesms.util.AppStartup;
import org.thoughtcrime.securesms.util.DeviceProperties;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.RemoteConfig;
import org.thoughtcrime.securesms.util.SignalLocalMetrics;
import org.thoughtcrime.securesms.util.SignalUncaughtExceptionHandler;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.signal.core.util.Util;
import org.thoughtcrime.securesms.util.VersionTracker;
import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageContextWrapper;
import org.whispersystems.signalservice.api.websocket.SignalWebSocket;
@@ -241,7 +242,7 @@ public class ApplicationContext extends Application implements AppForegroundObse
@Override
public void onForeground() {
long startTime = System.currentTimeMillis();
Log.i(TAG, "App is now visible.");
Log.i(TAG, "App is now visible. Battery: " + DeviceProperties.getBatteryLevel(this) + "% (charging: " + DeviceProperties.isCharging(this) + ")");
AppDependencies.getFrameRateTracker().start();
AppDependencies.getMegaphoneRepository().onAppForegrounded();

View File

@@ -11,7 +11,6 @@ import androidx.lifecycle.Observer;
import com.bumptech.glide.RequestManager;
import org.signal.ringrtc.CallLinkEpoch;
import org.signal.ringrtc.CallLinkRootKey;
import org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackState;
import org.thoughtcrime.securesms.contactshare.Contact;
@@ -30,8 +29,8 @@ import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.groups.GroupMigrationMembershipChange;
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
import org.thoughtcrime.securesms.mediapreview.MediaIntentFactory;
import org.thoughtcrime.securesms.polls.PollRecord;
import org.thoughtcrime.securesms.polls.PollOption;
import org.thoughtcrime.securesms.polls.PollRecord;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.stickers.StickerLocator;
@@ -136,7 +135,7 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable,
void goToMediaPreview(ConversationItem parent, View sharedElement, MediaIntentFactory.MediaPreviewArgs args);
void onEditedIndicatorClicked(@NonNull ConversationMessage conversationMessage);
void onShowGroupDescriptionClicked(@NonNull String groupName, @NonNull String description, boolean shouldLinkifyWebLinks);
void onJoinCallLink(@NonNull CallLinkRootKey callLinkRootKey, @Nullable CallLinkEpoch callLinkEpoch);
void onJoinCallLink(@NonNull CallLinkRootKey callLinkRootKey);
void onShowSafetyTips(boolean forGroup);
void onReportSpamLearnMoreClicked();
void onMessageRequestAcceptOptionsClicked();

View File

@@ -18,6 +18,7 @@ package org.thoughtcrime.securesms;
import android.Manifest;
import org.signal.core.ui.logging.LoggingFragment;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Rect;
@@ -71,7 +72,7 @@ import org.thoughtcrime.securesms.database.RecipientTable;
import org.thoughtcrime.securesms.groups.SelectionLimits;
import org.thoughtcrime.securesms.groups.ui.GroupLimitDialog;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.signal.core.ui.permissions.Permissions;
import org.thoughtcrime.securesms.profiles.manage.UsernameRepository;
import org.thoughtcrime.securesms.profiles.manage.UsernameRepository.UsernameAciFetchResult;
import org.thoughtcrime.securesms.recipients.Recipient;
@@ -341,6 +342,7 @@ public final class ContactSelectionListFragment extends LoggingFragment {
.map(r -> new ContactSearchKey.RecipientSearchKey(r, false))
.collect(java.util.stream.Collectors.toSet()),
selectionLimit,
isMulti,
new ContactSearchAdapter.DisplayOptions(
isMulti,
ContactSearchAdapter.DisplaySecondaryInformation.ALWAYS,
@@ -557,6 +559,7 @@ public final class ContactSelectionListFragment extends LoggingFragment {
public void onDataRefreshed() {
this.resetPositionOnCommit = true;
swipeRefresh.setRefreshing(false);
contactSearchMediator.refresh();
}
public boolean hasQueryFilter() {
@@ -573,6 +576,7 @@ public final class ContactSelectionListFragment extends LoggingFragment {
public void reset() {
contactSearchMediator.clearSelection();
contactSearchMediator.refresh();
fastScroller.setVisibility(View.GONE);
headerActionView.setVisibility(View.GONE);
}

View File

@@ -22,9 +22,9 @@ import androidx.fragment.app.setFragmentResult
import org.signal.core.ui.BottomSheetUtil
import org.signal.core.ui.compose.BottomSheets
import org.signal.core.ui.compose.Buttons
import org.signal.core.ui.compose.ComposeBottomSheetDialogFragment
import org.signal.core.ui.compose.DayNightPreviews
import org.signal.core.ui.compose.Previews
import org.thoughtcrime.securesms.compose.ComposeBottomSheetDialogFragment
/**
* Education sheet shown before authentication explaining that users should use their device credentials

View File

@@ -28,11 +28,11 @@ import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.dp
import org.signal.core.ui.compose.ComposeFragment
import org.signal.core.ui.compose.DayNightPreviews
import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.Scaffolds
import org.signal.core.ui.compose.SignalIcons
import org.thoughtcrime.securesms.compose.ComposeFragment
/**
* Fragment when inviting someone to use Signal

View File

@@ -85,6 +85,10 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.signal.core.ui.BottomSheetUtil
import org.signal.core.ui.compose.Snackbars
import org.signal.core.ui.compose.theme.SignalTheme
import org.signal.core.ui.isSplitPane
import org.signal.core.ui.permissions.Permissions
import org.signal.core.util.Util
import org.signal.core.util.concurrent.LifecycleDisposable
import org.signal.core.util.getSerializableCompat
import org.signal.core.util.logging.Log
@@ -93,7 +97,6 @@ import org.signal.mediasend.MediaSendActivityContract
import org.thoughtcrime.securesms.backup.v2.ArchiveRestoreProgress
import org.thoughtcrime.securesms.backup.v2.ui.verify.VerifyBackupKeyActivity
import org.thoughtcrime.securesms.calls.YouAreAlreadyInACallSnackbar.show
import org.thoughtcrime.securesms.calls.links.details.CallLinkDetailsActivity
import org.thoughtcrime.securesms.calls.log.CallLogFilter
import org.thoughtcrime.securesms.calls.log.CallLogFragment
import org.thoughtcrime.securesms.calls.new.NewCallActivity
@@ -113,7 +116,6 @@ import org.thoughtcrime.securesms.components.snackbars.SnackbarHostKey
import org.thoughtcrime.securesms.components.snackbars.SnackbarState
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaController
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaControllerOwner
import org.thoughtcrime.securesms.compose.SignalTheme
import org.thoughtcrime.securesms.conversation.ConversationIntents
import org.thoughtcrime.securesms.conversation.NewConversationActivity
import org.thoughtcrime.securesms.conversation.v2.MotionEventRelay
@@ -137,6 +139,7 @@ import org.thoughtcrime.securesms.main.MainContentLayoutData
import org.thoughtcrime.securesms.main.MainMegaphoneState
import org.thoughtcrime.securesms.main.MainNavigationBar
import org.thoughtcrime.securesms.main.MainNavigationDetailLocation
import org.thoughtcrime.securesms.main.MainNavigationDetailLocationEffect
import org.thoughtcrime.securesms.main.MainNavigationListLocation
import org.thoughtcrime.securesms.main.MainNavigationRail
import org.thoughtcrime.securesms.main.MainNavigationViewModel
@@ -153,11 +156,9 @@ import org.thoughtcrime.securesms.main.chatNavGraphBuilder
import org.thoughtcrime.securesms.main.navigateToDetailLocation
import org.thoughtcrime.securesms.main.rememberDetailNavHostController
import org.thoughtcrime.securesms.main.rememberFocusRequester
import org.thoughtcrime.securesms.main.rememberMainNavigationDetailLocation
import org.thoughtcrime.securesms.main.storiesNavGraphBuilder
import org.thoughtcrime.securesms.mediasend.camerax.CameraXUtil
import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionActivity
import org.thoughtcrime.securesms.mediasend.v3.MediaSendV3ActivityContract
import org.thoughtcrime.securesms.megaphone.Megaphone
import org.thoughtcrime.securesms.megaphone.MegaphoneActionController
import org.thoughtcrime.securesms.megaphone.Megaphones
@@ -165,7 +166,6 @@ import org.thoughtcrime.securesms.net.DeviceTransferBlockingInterceptor
import org.thoughtcrime.securesms.notifications.VitalsViewModel
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfiles
import org.thoughtcrime.securesms.permissions.Permissions
import org.thoughtcrime.securesms.profiles.manage.UsernameEditFragment
import org.thoughtcrime.securesms.service.BackupMediaRestoreService
import org.thoughtcrime.securesms.service.KeyCachingService
@@ -181,14 +181,12 @@ import org.thoughtcrime.securesms.util.DynamicTheme
import org.thoughtcrime.securesms.util.Material3OnScrollHelper
import org.thoughtcrime.securesms.util.SplashScreenUtil
import org.thoughtcrime.securesms.util.TopToastPopup
import org.thoughtcrime.securesms.util.Util
import org.thoughtcrime.securesms.util.viewModel
import org.thoughtcrime.securesms.window.AppPaneDragHandle
import org.thoughtcrime.securesms.window.AppScaffold
import org.thoughtcrime.securesms.window.AppScaffoldAnimationStateFactory
import org.thoughtcrime.securesms.window.AppScaffoldNavigator
import org.thoughtcrime.securesms.window.NavigationType
import org.thoughtcrime.securesms.window.isSplitPane
import org.thoughtcrime.securesms.window.rememberThreePaneScaffoldNavigatorDelegate
import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState
import org.signal.core.ui.R as CoreUiR
@@ -280,7 +278,7 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
super.onCreate(savedInstanceState, ready)
navigator = MainNavigator(this, mainNavigationViewModel)
mediaActivityLauncher = registerForActivityResult(MediaSendV3ActivityContract()) { }
mediaActivityLauncher = registerForActivityResult(MediaSendActivityContract()) { }
AppForegroundObserver.addListener(object : AppForegroundObserver.Listener {
override fun onForeground() {
@@ -448,7 +446,7 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
val chatNavGraphState = ChatNavGraphState.remember(windowSizeClass)
val mutableInteractionSource = remember { MutableInteractionSource() }
val mainNavigationDetailLocation by rememberMainNavigationDetailLocation(mainNavigationViewModel, chatNavGraphState::writeGraphicsLayerToBitmap)
MainNavigationDetailLocationEffect(mainNavigationViewModel, chatNavGraphState::writeGraphicsLayerToBitmap)
val chatsNavHostController = rememberDetailNavHostController(
onRequestFocus = rememberFocusRequester(
@@ -478,25 +476,33 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
storiesNavGraphBuilder()
}
LaunchedEffect(mainNavigationDetailLocation) {
mainNavigationViewModel.clearEarlyDetailLocation()
when (mainNavigationDetailLocation) {
is MainNavigationDetailLocation.Empty -> {
when (mainNavigationState.currentListLocation) {
MainNavigationListLocation.CHATS, MainNavigationListLocation.ARCHIVE -> chatsNavHostController
MainNavigationListLocation.CALLS -> callsNavHostController
MainNavigationListLocation.STORIES -> storiesNavHostController
}.navigateToDetailLocation(mainNavigationDetailLocation)
}
LaunchedEffect(Unit) {
suspend fun navigateToLocation(location: MainNavigationDetailLocation) {
when (location) {
is MainNavigationDetailLocation.Empty -> {
when (mainNavigationState.currentListLocation) {
MainNavigationListLocation.CHATS, MainNavigationListLocation.ARCHIVE -> chatsNavHostController
MainNavigationListLocation.CALLS -> callsNavHostController
MainNavigationListLocation.STORIES -> storiesNavHostController
}.navigateToDetailLocation(location)
}
is MainNavigationDetailLocation.Chats -> {
chatNavGraphState.writeGraphicsLayerToBitmap()
chatsNavHostController.navigateToDetailLocation(mainNavigationDetailLocation)
}
is MainNavigationDetailLocation.Chats -> {
if (location is MainNavigationDetailLocation.Chats.Conversation) {
chatNavGraphState.writeGraphicsLayerToBitmap()
}
chatsNavHostController.navigateToDetailLocation(location)
}
is MainNavigationDetailLocation.Calls -> callsNavHostController.navigateToDetailLocation(mainNavigationDetailLocation)
is MainNavigationDetailLocation.Stories -> storiesNavHostController.navigateToDetailLocation(mainNavigationDetailLocation)
is MainNavigationDetailLocation.Calls -> callsNavHostController.navigateToDetailLocation(location)
is MainNavigationDetailLocation.Stories -> storiesNavHostController.navigateToDetailLocation(location)
}
}
mainNavigationViewModel.earlyNavigationDetailLocationRequested?.let { navigateToLocation(it) }
mainNavigationViewModel.clearEarlyDetailLocation()
mainNavigationViewModel.detailLocation.collect { navigateToLocation(it) }
}
val scope = rememberCoroutineScope()
@@ -753,27 +759,7 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
val coroutine = rememberCoroutineScope()
return remember(scaffoldNavigator, coroutine) {
mainNavigationViewModel.wrapNavigator(coroutine, scaffoldNavigator) { detailLocation ->
when (detailLocation) {
is MainNavigationDetailLocation.Chats.Conversation -> {
startActivity(
ConversationIntents.createBuilderSync(this, detailLocation.conversationArgs.recipientId, detailLocation.conversationArgs.threadId)
.withArgs(detailLocation.conversationArgs)
.build()
)
}
is MainNavigationDetailLocation.Calls.CallLinks.CallLinkDetails -> {
startActivity(CallLinkDetailsActivity.createIntent(this, detailLocation.callLinkRoomId))
}
is MainNavigationDetailLocation.Calls.CallLinks.EditCallLinkName -> {
error("Unexpected subroute EditCallLinkName.")
}
MainNavigationDetailLocation.Empty -> Unit
}
}
mainNavigationViewModel.wrapNavigator(coroutine, scaffoldNavigator)
}
}
@@ -782,7 +768,7 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass
CompositionLocalProvider(LocalSnackbarStateConsumerRegistry provides mainNavigationViewModel.snackbarRegistry) {
SignalTheme(isDarkMode = DynamicTheme.isDarkTheme(this)) {
SignalTheme {
val backgroundColor = if (!windowSizeClass.isSplitPane()) {
MaterialTheme.colorScheme.surface
} else {

View File

@@ -4,6 +4,8 @@ import android.content.Context;
import androidx.annotation.NonNull;
import org.signal.core.ui.logging.LoggingFragment;
public class MainFragment extends LoggingFragment {
@Override

View File

@@ -9,9 +9,9 @@ import android.content.Context
import android.text.TextUtils
import okio.ByteString.Companion.toByteString
import org.signal.core.util.Base64
import org.signal.core.util.Util
import org.signal.core.util.toByteArray
import org.thoughtcrime.securesms.util.MediaUtil
import org.thoughtcrime.securesms.util.Util
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId
import org.whispersystems.signalservice.internal.push.AttachmentPointer
import java.io.IOException

View File

@@ -13,6 +13,8 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
import io.reactivex.rxjava3.core.Completable
import kotlinx.coroutines.rx3.rxCompletable
import kotlinx.coroutines.withContext
import org.signal.core.ui.permissions.Permissions
import org.signal.core.ui.util.StorageUtil
import org.signal.core.ui.view.AlertDialogResult
import org.signal.core.ui.view.awaitResult
import org.signal.core.util.concurrent.SignalDispatchers
@@ -24,11 +26,9 @@ import org.thoughtcrime.securesms.components.ProgressCardDialogFragmentArgs
import org.thoughtcrime.securesms.database.MediaTable
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.permissions.Permissions
import org.thoughtcrime.securesms.util.SaveAttachmentUtil
import org.thoughtcrime.securesms.util.SaveAttachmentUtil.SaveAttachment
import org.thoughtcrime.securesms.util.SaveAttachmentUtil.SaveAttachmentsResult
import org.thoughtcrime.securesms.util.StorageUtil
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine

View File

@@ -12,7 +12,7 @@ import android.os.Process;
import org.signal.core.util.StreamUtil;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.util.Util;
import org.signal.core.util.Util;
import java.io.IOException;
import java.io.OutputStream;

View File

@@ -17,6 +17,7 @@ import androidx.fragment.app.viewModels
import androidx.navigation.Navigation
import androidx.recyclerview.widget.RecyclerView
import org.signal.core.models.media.Media
import org.signal.core.ui.permissions.Permissions
import org.signal.core.util.ThreadUtil
import org.signal.core.util.getParcelableExtraCompat
import org.thoughtcrime.securesms.R
@@ -30,7 +31,6 @@ import org.thoughtcrime.securesms.components.ButtonStripItemView
import org.thoughtcrime.securesms.components.recyclerview.GridDividerDecoration
import org.thoughtcrime.securesms.mediasend.AvatarSelectionActivity
import org.thoughtcrime.securesms.mediasend.camerax.CameraXUtil
import org.thoughtcrime.securesms.permissions.Permissions
import org.thoughtcrime.securesms.util.ViewUtil
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
import org.thoughtcrime.securesms.util.navigation.safeNavigate

View File

@@ -32,7 +32,7 @@ import org.thoughtcrime.securesms.restore.restorelocalbackup.PassphraseAsYouType
import org.thoughtcrime.securesms.service.LocalBackupListener;
import org.thoughtcrime.securesms.util.BackupUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.signal.core.util.Util;
import org.thoughtcrime.securesms.util.text.AfterTextChanged;
public class BackupDialog {

View File

@@ -21,7 +21,7 @@ import org.thoughtcrime.securesms.backup.proto.KeyValue;
import org.thoughtcrime.securesms.backup.proto.SharedPreference;
import org.thoughtcrime.securesms.backup.proto.SqlStatement;
import org.thoughtcrime.securesms.backup.proto.Sticker;
import org.thoughtcrime.securesms.util.Util;
import org.signal.core.util.Util;
import java.io.IOException;
import java.io.InputStream;

View File

@@ -41,7 +41,7 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.profiles.AvatarHelper;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.BackupUtil;
import org.thoughtcrime.securesms.util.Util;
import org.signal.core.util.Util;
import java.io.ByteArrayOutputStream;
import java.io.File;

View File

@@ -10,12 +10,12 @@ import org.signal.core.models.backup.MediaName
import org.signal.core.util.Base64
import org.signal.core.util.Base64.decodeBase64
import org.signal.core.util.Base64.decodeBase64OrThrow
import org.signal.core.util.Util
import org.thoughtcrime.securesms.attachments.DatabaseAttachment
import org.thoughtcrime.securesms.attachments.InvalidAttachmentException
import org.thoughtcrime.securesms.database.AttachmentTable
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.util.RemoteConfig
import org.thoughtcrime.securesms.util.Util
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId
import java.io.IOException

View File

@@ -40,7 +40,6 @@ class CallLinkArchiveExporter(private val cursor: Cursor) : Iterator<ArchiveReci
id = callLink.recipientId.toLong(),
callLink = CallLink(
rootKey = callLink.credentials!!.linkKeyBytes.toByteString(),
epoch = callLink.credentials.epochBytes?.takeIf { it.size == 4 }?.toByteString(),
adminKey = callLink.credentials.adminPassBytes?.toByteString()?.nullIfEmpty(),
name = callLink.state.name,
expirationMs = expirationTime.takeIf { it != Long.MAX_VALUE }?.clampToValidBackupRange() ?: 0,

View File

@@ -123,7 +123,13 @@ private fun Member.Role.toRemote(): Group.Member.Role {
}
private fun DecryptedMember.toRemote(): Group.Member {
return Group.Member(userId = aciBytes, role = role.toRemote(), joinedAtVersion = joinedAtRevision)
return Group.Member(
userId = aciBytes,
role = role.toRemote(),
joinedAtVersion = joinedAtRevision,
labelEmoji = labelEmoji,
labelString = labelString
)
}
private fun DecryptedPendingMember.toRemote(): Group.MemberPendingProfileKey {

View File

@@ -42,11 +42,7 @@ object CallLinkArchiveImporter {
CallLinkTable.CallLink(
recipientId = RecipientId.UNKNOWN,
roomId = CallLinkRoomId.fromCallLinkRootKey(rootKey),
credentials = CallLinkCredentials(
callLink.rootKey.toByteArray(),
callLink.epoch?.toByteArray(),
callLink.adminKey?.toByteArray()
),
credentials = CallLinkCredentials(callLink.rootKey.toByteArray(), callLink.adminKey?.toByteArray()),
state = SignalCallLinkState(
name = callLink.name,
restrictions = callLink.restrictions.toLocal(),

View File

@@ -116,7 +116,13 @@ private fun Group.Member.Role.toLocal(): Member.Role {
}
private fun Group.Member.toLocal(): DecryptedMember {
return DecryptedMember(aciBytes = userId, role = role.toLocal(), joinedAtRevision = joinedAtVersion)
return DecryptedMember(
aciBytes = userId,
role = role.toLocal(),
joinedAtRevision = joinedAtVersion,
labelEmoji = labelEmoji,
labelString = labelString
)
}
private fun Group.MemberPendingAdminApproval.toLocal(): DecryptedRequestingMember {

View File

@@ -11,6 +11,7 @@ import org.signal.core.models.backup.BackupId
import org.signal.core.models.backup.MediaName
import org.signal.core.util.Stopwatch
import org.signal.core.util.StreamUtil
import org.signal.core.util.Util
import org.signal.core.util.logging.Log
import org.signal.core.util.readFully
import org.thoughtcrime.securesms.attachments.AttachmentId
@@ -20,7 +21,6 @@ import org.thoughtcrime.securesms.backup.v2.local.proto.FilesFrame
import org.thoughtcrime.securesms.backup.v2.local.proto.Metadata
import org.thoughtcrime.securesms.database.AttachmentTable
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.util.Util
import org.whispersystems.signalservice.api.crypto.AttachmentCipherOutputStream
import org.whispersystems.signalservice.api.crypto.AttachmentCipherStreamUtil
import org.whispersystems.signalservice.internal.crypto.PaddingInputStream

View File

@@ -7,13 +7,13 @@ package org.thoughtcrime.securesms.backup.v2.stream
import org.signal.core.models.ServiceId.ACI
import org.signal.core.models.backup.MessageBackupKey
import org.signal.core.util.Util
import org.signal.core.util.stream.MacOutputStream
import org.signal.core.util.writeVarInt32
import org.signal.libsignal.messagebackup.BackupForwardSecrecyToken
import org.thoughtcrime.securesms.backup.v2.proto.BackupInfo
import org.thoughtcrime.securesms.backup.v2.proto.Frame
import org.thoughtcrime.securesms.backup.v2.stream.EncryptedBackupReader.Companion.createForSignalBackup
import org.thoughtcrime.securesms.util.Util
import java.io.IOException
import java.io.OutputStream
import javax.crypto.Cipher

View File

@@ -37,6 +37,7 @@ import androidx.core.os.bundleOf
import androidx.fragment.app.DialogFragment
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.parcelize.Parcelize
import org.signal.core.ui.compose.ComposeBottomSheetDialogFragment
import org.signal.core.ui.compose.DayNightPreviews
import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.theme.SignalTheme
@@ -45,7 +46,6 @@ import org.thoughtcrime.securesms.backup.v2.BackupRepository
import org.thoughtcrime.securesms.billing.launchManageBackupsSubscription
import org.thoughtcrime.securesms.components.contactsupport.ContactSupportDialogFragment
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity
import org.thoughtcrime.securesms.compose.ComposeBottomSheetDialogFragment
import org.thoughtcrime.securesms.jobs.BackupMessagesJob
import org.thoughtcrime.securesms.keyvalue.protos.BackupDownloadNotifierState
import org.thoughtcrime.securesms.util.CommunicationActions

View File

@@ -27,11 +27,11 @@ import androidx.core.os.bundleOf
import androidx.fragment.app.setFragmentResult
import org.signal.core.ui.compose.BottomSheets
import org.signal.core.ui.compose.Buttons
import org.signal.core.ui.compose.ComposeBottomSheetDialogFragment
import org.signal.core.ui.compose.DayNightPreviews
import org.signal.core.ui.compose.Previews
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.app.backups.BackupStateObserver
import org.thoughtcrime.securesms.compose.ComposeBottomSheetDialogFragment
import org.thoughtcrime.securesms.jobs.BackupMessagesJob
import org.signal.core.ui.R as CoreUiR

View File

@@ -11,12 +11,12 @@ import androidx.compose.ui.res.stringResource
import androidx.core.os.BundleCompat
import androidx.core.os.bundleOf
import androidx.fragment.app.DialogFragment
import org.signal.core.ui.compose.ComposeDialogFragment
import org.signal.core.ui.compose.DayNightPreviews
import org.signal.core.ui.compose.Dialogs
import org.signal.core.ui.compose.Previews
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.backup.v2.BackupRepository
import org.thoughtcrime.securesms.compose.ComposeDialogFragment
/**
* Displays a "last chance" dialog to the user to begin a media restore.

View File

@@ -12,11 +12,11 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import org.signal.core.ui.compose.ComposeBottomSheetDialogFragment
import org.signal.core.ui.compose.DayNightPreviews
import org.signal.core.ui.compose.Previews
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity
import org.thoughtcrime.securesms.compose.ComposeBottomSheetDialogFragment
import org.thoughtcrime.securesms.jobs.BackupMessagesJob
import org.thoughtcrime.securesms.keyvalue.SignalStore
import kotlin.time.Duration

View File

@@ -19,12 +19,12 @@ import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.withLink
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import org.signal.core.ui.compose.ComposeBottomSheetDialogFragment
import org.signal.core.ui.compose.DayNightPreviews
import org.signal.core.ui.compose.Previews
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.backup.v2.BackupRepository
import org.thoughtcrime.securesms.components.contactsupport.ContactSupportDialogFragment
import org.thoughtcrime.securesms.compose.ComposeBottomSheetDialogFragment
import org.thoughtcrime.securesms.util.CommunicationActions
class NoRemoteStorageSpaceAvailableBottomSheet : ComposeBottomSheetDialogFragment() {

View File

@@ -33,8 +33,6 @@ import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.Scaffolds
import org.signal.core.ui.compose.SignalIcons
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.compose.BetaHeader
import org.thoughtcrime.securesms.components.compose.TextWithBetaLabel
import org.signal.core.ui.R as CoreUiR
/**
@@ -65,10 +63,6 @@ fun MessageBackupsEducationScreen(
.fillMaxWidth()
.weight(1f)
) {
item {
BetaHeader()
}
item {
Image(
painter = painterResource(id = R.drawable.image_signal_backups),
@@ -80,9 +74,9 @@ fun MessageBackupsEducationScreen(
}
item {
TextWithBetaLabel(
Text(
text = stringResource(id = R.string.RemoteBackupsSettingsFragment__signal_backups),
textStyle = MaterialTheme.typography.headlineMedium,
style = MaterialTheme.typography.headlineMedium,
modifier = Modifier.padding(top = 15.dp)
)
}

View File

@@ -29,21 +29,21 @@ import com.google.android.gms.common.GoogleApiAvailability
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import kotlinx.coroutines.rx3.asFlowable
import org.signal.core.ui.compose.ComposeFragment
import org.signal.core.ui.compose.Dialogs
import org.signal.core.util.Util
import org.signal.core.util.concurrent.SignalDispatchers
import org.signal.core.util.getSerializableCompat
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.backup.DeletionState
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.InAppPaymentCheckoutDelegate
import org.thoughtcrime.securesms.compose.ComposeFragment
import org.thoughtcrime.securesms.compose.Nav
import org.thoughtcrime.securesms.database.InAppPaymentTable
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.util.CommunicationActions
import org.thoughtcrime.securesms.util.PlayStoreUtil
import org.thoughtcrime.securesms.util.Util
import org.thoughtcrime.securesms.util.storage.AndroidCredentialRepository
import org.thoughtcrime.securesms.util.viewModel

View File

@@ -44,8 +44,8 @@ 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.horizontalGutters
import org.signal.core.ui.compose.theme.SignalTheme
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.compose.SignalTheme
import org.signal.core.ui.R as CoreUiR
enum class MessageBackupsKeyEducationScreenMode {

View File

@@ -57,12 +57,12 @@ import org.signal.core.ui.compose.SignalIcons
import org.signal.core.ui.compose.Snackbars
import org.signal.core.ui.compose.horizontalGutters
import org.signal.core.ui.compose.theme.SignalTheme
import org.signal.core.util.Util
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.app.backups.remote.BackupKeyCredentialManagerHandler
import org.thoughtcrime.securesms.components.settings.app.backups.remote.BackupKeySaveState
import org.thoughtcrime.securesms.fonts.MonoTypeface
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.util.Util
import org.thoughtcrime.securesms.util.storage.AndroidCredentialRepository
import org.thoughtcrime.securesms.util.storage.CredentialManagerError
import org.thoughtcrime.securesms.util.storage.CredentialManagerResult

View File

@@ -2,8 +2,8 @@ package org.thoughtcrime.securesms.backup.v2.ui.verify
import android.app.Activity.RESULT_OK
import androidx.compose.runtime.Composable
import org.signal.core.ui.compose.ComposeFragment
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsKeyVerifyScreen
import org.thoughtcrime.securesms.compose.ComposeFragment
import org.thoughtcrime.securesms.keyvalue.SignalStore
/**

View File

@@ -5,9 +5,9 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import org.signal.core.ui.compose.ComposeFragment
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsKeyRecordMode
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsKeyRecordScreen
import org.thoughtcrime.securesms.compose.ComposeFragment
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.util.viewModel

View File

@@ -24,11 +24,11 @@ import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import org.signal.core.ui.compose.DayNightPreviews
import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.theme.SignalTheme
import org.thoughtcrime.securesms.PassphraseRequiredActivity
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.backup.v2.ui.subscription.EnterKeyScreen
import org.thoughtcrime.securesms.components.compose.rememberBiometricsAuthentication
import org.thoughtcrime.securesms.compose.SignalTheme
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.util.CommunicationActions
import kotlin.random.Random

View File

@@ -32,17 +32,17 @@ import androidx.fragment.app.FragmentManager
import org.signal.core.ui.BottomSheetUtil
import org.signal.core.ui.compose.BottomSheets
import org.signal.core.ui.compose.Buttons
import org.signal.core.ui.compose.ComposeBottomSheetDialogFragment
import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.SignalIcons
import org.signal.core.ui.compose.Texts
import org.signal.core.ui.compose.theme.SignalTheme
import org.signal.core.util.getParcelableCompat
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.badges.models.Badge
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity
import org.thoughtcrime.securesms.components.settings.app.subscription.BadgeImage112
import org.thoughtcrime.securesms.components.settings.app.subscription.manage.ManageDonationsFragment
import org.thoughtcrime.securesms.compose.ComposeBottomSheetDialogFragment
import org.thoughtcrime.securesms.compose.SignalTheme
import org.thoughtcrime.securesms.database.InAppPaymentTable
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.util.CommunicationActions

View File

@@ -12,12 +12,12 @@ import androidx.fragment.app.viewModels
import androidx.viewpager2.widget.ViewPager2
import com.google.android.material.tabs.TabLayoutMediator
import org.signal.core.ui.BottomSheetUtil
import org.signal.core.ui.FixedRoundedCornerBottomSheetDialogFragment
import org.signal.core.util.getParcelableCompat
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.badges.BadgeRepository
import org.thoughtcrime.securesms.badges.models.Badge
import org.thoughtcrime.securesms.badges.models.LargeBadge
import org.thoughtcrime.securesms.components.FixedRoundedCornerBottomSheetDialogFragment
import org.thoughtcrime.securesms.components.ViewBinderDelegate
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppDonations

View File

@@ -15,8 +15,8 @@ import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import kotlinx.coroutines.flow.Flow
import org.signal.core.ui.compose.theme.SignalTheme
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.compose.SignalTheme
/**
* A class that can be instantiated with a list of [Flow]s that produce [Banner]s, then applied to a [ComposeView], typically within a [Fragment].

View File

@@ -15,6 +15,7 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
import org.signal.core.ui.compose.DayNightPreviews
import org.signal.core.ui.compose.Previews
import org.signal.core.util.Util
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.banner.Banner
import org.thoughtcrime.securesms.banner.ui.compose.Action
@@ -22,7 +23,6 @@ import org.thoughtcrime.securesms.banner.ui.compose.DefaultBanner
import org.thoughtcrime.securesms.banner.ui.compose.Importance
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.util.PlayStoreUtil
import org.thoughtcrime.securesms.util.Util
import kotlin.time.Duration.Companion.milliseconds
/**

View File

@@ -23,6 +23,7 @@ import com.google.android.gms.common.GoogleApiAvailability
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import kotlinx.coroutines.rx3.asFlowable
import org.signal.core.ui.compose.ComposeBottomSheetDialogFragment
import org.signal.core.ui.compose.Dialogs
import org.signal.core.util.concurrent.SignalDispatchers
import org.thoughtcrime.securesms.R
@@ -31,7 +32,6 @@ import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsFlowVi
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsStage
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.InAppPaymentCheckoutDelegate
import org.thoughtcrime.securesms.compose.ComposeBottomSheetDialogFragment
import org.thoughtcrime.securesms.database.InAppPaymentTable
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.util.viewModel

View File

@@ -8,7 +8,6 @@ package org.thoughtcrime.securesms.calls.links
import io.reactivex.rxjava3.core.Observable
import org.signal.core.util.logging.Log
import org.signal.ringrtc.CallException
import org.signal.ringrtc.CallLinkEpoch
import org.signal.ringrtc.CallLinkRootKey
import org.thoughtcrime.securesms.database.CallLinkTable
import org.thoughtcrime.securesms.database.DatabaseObserver
@@ -23,7 +22,6 @@ import java.net.URLDecoder
*/
object CallLinks {
private const val ROOT_KEY = "key"
private const val EPOCH = "epoch"
private const val LEGACY_HTTPS_LINK_PREFIX = "https://signal.link/call#key="
private const val LEGACY_SGNL_LINK_PREFIX = "sgnl://signal.link/call#key="
private const val HTTPS_LINK_PREFIX = "https://signal.link/call/#key="
@@ -31,13 +29,7 @@ object CallLinks {
private val TAG = Log.tag(CallLinks::class.java)
fun url(rootKeyBytes: ByteArray, epochBytes: ByteArray?): String {
return if (epochBytes == null) {
"$HTTPS_LINK_PREFIX${CallLinkRootKey(rootKeyBytes)}"
} else {
"$HTTPS_LINK_PREFIX${CallLinkRootKey(rootKeyBytes)}&epoch=${CallLinkEpoch.fromBytes(epochBytes)}"
}
}
fun url(rootKeyBytes: ByteArray): String = "$HTTPS_LINK_PREFIX${CallLinkRootKey(rootKeyBytes)}"
fun watchCallLink(roomId: CallLinkRoomId): Observable<CallLinkTable.CallLink> {
return Observable.create { emitter ->
@@ -78,13 +70,8 @@ object CallLinks {
return url.split("#").last().startsWith("key=")
}
data class CallLinkParseResult(
val rootKey: CallLinkRootKey,
val epoch: CallLinkEpoch?
)
@JvmStatic
fun parseUrl(url: String): CallLinkParseResult? {
fun parseUrl(url: String): CallLinkRootKey? {
if (!isPrefixedCallLink(url)) {
Log.w(TAG, "Invalid url prefix.")
return null
@@ -132,13 +119,9 @@ object CallLinks {
}
return try {
val epoch = fragmentQuery[EPOCH]?.let { s -> CallLinkEpoch(s) }
CallLinkParseResult(
rootKey = CallLinkRootKey(key),
epoch = epoch
)
return CallLinkRootKey(key)
} catch (e: CallException) {
Log.w(TAG, "Invalid root key or epoch found in fragment query string.")
Log.w(TAG, "Invalid root key found in fragment query string.")
null
}
}

View File

@@ -40,14 +40,14 @@ import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.viewmodel.compose.viewModel
import kotlinx.coroutines.launch
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.isSplitPane
import org.signal.core.util.BreakIteratorCompat
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.calls.links.details.CallLinkDetailsViewModel
import org.thoughtcrime.securesms.compose.ComposeDialogFragment
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkRoomId
import org.thoughtcrime.securesms.window.isSplitPane
class EditCallLinkNameDialogFragment : ComposeDialogFragment() {

View File

@@ -53,7 +53,7 @@ import org.signal.core.ui.R as CoreUiR
@Composable
private fun SignalCallRowPreview() {
val callLink = remember {
val credentials = CallLinkCredentials(byteArrayOf(1, 2, 3, 4), byteArrayOf(0, 1, 2, 3), byteArrayOf(5, 6, 7, 8))
val credentials = CallLinkCredentials(byteArrayOf(1, 2, 3, 4), byteArrayOf(5, 6, 7, 8))
CallLinkTable.CallLink(
recipientId = RecipientId.UNKNOWN,
roomId = CallLinkRoomId.fromBytes(byteArrayOf(1, 3, 5, 7)),
@@ -97,7 +97,7 @@ fun SignalCallRow(
"https://signal.call.example.com"
} else {
remember(callLink.credentials) {
callLink.credentials?.let { CallLinks.url(it.linkKeyBytes, it.epochBytes) } ?: ""
callLink.credentials?.let { CallLinks.url(it.linkKeyBytes) } ?: ""
}
}

View File

@@ -37,11 +37,13 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import io.reactivex.rxjava3.kotlin.subscribeBy
import org.signal.core.ui.compose.BottomSheets
import org.signal.core.ui.compose.Buttons
import org.signal.core.ui.compose.ComposeBottomSheetDialogFragment
import org.signal.core.ui.compose.DayNightPreviews
import org.signal.core.ui.compose.Dividers
import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.Rows
import org.signal.core.ui.compose.SignalIcons
import org.signal.core.util.Util
import org.signal.core.util.concurrent.LifecycleDisposable
import org.signal.core.util.logging.Log
import org.signal.ringrtc.CallLinkState
@@ -50,7 +52,6 @@ import org.thoughtcrime.securesms.calls.YouAreAlreadyInACallSnackbar.YouAreAlrea
import org.thoughtcrime.securesms.calls.links.CallLinks
import org.thoughtcrime.securesms.calls.links.EditCallLinkNameDialogFragment
import org.thoughtcrime.securesms.calls.links.SignalCallRow
import org.thoughtcrime.securesms.compose.ComposeBottomSheetDialogFragment
import org.thoughtcrime.securesms.database.CallLinkTable
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkRoomId
@@ -59,7 +60,6 @@ import org.thoughtcrime.securesms.service.webrtc.links.SignalCallLinkState
import org.thoughtcrime.securesms.service.webrtc.links.UpdateCallLinkResult
import org.thoughtcrime.securesms.sharing.v2.ShareActivity
import org.thoughtcrime.securesms.util.CommunicationActions
import org.thoughtcrime.securesms.util.Util
import java.time.Instant
import org.signal.core.ui.R as CoreUiR
@@ -162,7 +162,7 @@ class CreateCallLinkBottomSheetDialogFragment : ComposeBottomSheetDialogFragment
startActivity(
ShareActivity.sendSimpleText(
requireContext(),
getString(R.string.CreateCallLink__use_this_link_to_join_a_signal_call, CallLinks.url(viewModel.linkKeyBytes, viewModel.epochBytes))
getString(R.string.CreateCallLink__use_this_link_to_join_a_signal_call, CallLinks.url(viewModel.linkKeyBytes))
)
)
}
@@ -176,7 +176,7 @@ class CreateCallLinkBottomSheetDialogFragment : ComposeBottomSheetDialogFragment
lifecycleDisposable += viewModel.commitCallLink().subscribeBy(onSuccess = {
when (it) {
is EnsureCallLinkCreatedResult.Success -> {
Util.copyToClipboard(requireContext(), CallLinks.url(viewModel.linkKeyBytes, viewModel.epochBytes))
Util.copyToClipboard(requireContext(), CallLinks.url(viewModel.linkKeyBytes))
Toast.makeText(requireContext(), R.string.CreateCallLinkBottomSheetDialogFragment__copied_to_clipboard, Toast.LENGTH_LONG).show()
}
@@ -191,7 +191,7 @@ class CreateCallLinkBottomSheetDialogFragment : ComposeBottomSheetDialogFragment
is EnsureCallLinkCreatedResult.Success -> {
val mimeType = Intent.normalizeMimeType("text/plain")
val shareIntent = ShareCompat.IntentBuilder(requireContext())
.setText(CallLinks.url(viewModel.linkKeyBytes, viewModel.epochBytes))
.setText(CallLinks.url(viewModel.linkKeyBytes))
.setType(mimeType)
.createChooserIntent()

View File

@@ -52,9 +52,6 @@ class CreateCallLinkViewModel(
val linkKeyBytes: ByteArray
get() = callLink.value.credentials!!.linkKeyBytes
val epochBytes: ByteArray?
get() = callLink.value.credentials!!.epochBytes
private val internalShowAlreadyInACall = MutableStateFlow(false)
val showAlreadyInACall: StateFlow<Boolean> = internalShowAlreadyInACall

View File

@@ -13,9 +13,9 @@ import androidx.activity.enableEdgeToEdge
import androidx.compose.runtime.remember
import androidx.core.os.bundleOf
import androidx.fragment.app.FragmentActivity
import org.signal.core.ui.compose.theme.SignalTheme
import org.signal.core.util.getParcelableExtraCompat
import org.thoughtcrime.securesms.calls.links.EditCallLinkNameDialogFragment
import org.thoughtcrime.securesms.compose.SignalTheme
import org.thoughtcrime.securesms.main.MainNavigationDetailLocation
import org.thoughtcrime.securesms.main.MainNavigationListLocation
import org.thoughtcrime.securesms.main.MainNavigationRouter

View File

@@ -37,6 +37,8 @@ import org.signal.core.ui.compose.Rows
import org.signal.core.ui.compose.Scaffolds
import org.signal.core.ui.compose.SignalIcons
import org.signal.core.ui.compose.Snackbars
import org.signal.core.ui.isSplitPane
import org.signal.core.util.Util
import org.signal.core.util.concurrent.LifecycleDisposable
import org.signal.ringrtc.CallLinkState.Restrictions
import org.thoughtcrime.securesms.R
@@ -53,8 +55,6 @@ import org.thoughtcrime.securesms.service.webrtc.links.CallLinkRoomId
import org.thoughtcrime.securesms.service.webrtc.links.SignalCallLinkState
import org.thoughtcrime.securesms.sharing.v2.ShareActivity
import org.thoughtcrime.securesms.util.CommunicationActions
import org.thoughtcrime.securesms.util.Util
import org.thoughtcrime.securesms.window.isSplitPane
import java.time.Instant
@Composable
@@ -119,7 +119,7 @@ class DefaultCallLinkDetailsCallback(
override fun onShareClicked() {
val mimeType = Intent.normalizeMimeType("text/plain")
val shareIntent = ShareCompat.IntentBuilder(activity)
.setText(CallLinks.url(viewModel.rootKeySnapshot, viewModel.epochSnapshot))
.setText(CallLinks.url(viewModel.rootKeySnapshot))
.setType(mimeType)
.createChooserIntent()
@@ -131,7 +131,7 @@ class DefaultCallLinkDetailsCallback(
}
override fun onCopyClicked() {
Util.copyToClipboard(activity, CallLinks.url(viewModel.rootKeySnapshot, viewModel.epochSnapshot))
Util.copyToClipboard(activity, CallLinks.url(viewModel.rootKeySnapshot))
Toast.makeText(activity, R.string.CreateCallLinkBottomSheetDialogFragment__copied_to_clipboard, Toast.LENGTH_LONG).show()
}
@@ -139,7 +139,7 @@ class DefaultCallLinkDetailsCallback(
activity.startActivity(
ShareActivity.sendSimpleText(
activity,
activity.getString(R.string.CreateCallLink__use_this_link_to_join_a_signal_call, CallLinks.url(viewModel.rootKeySnapshot, viewModel.epochSnapshot))
activity.getString(R.string.CreateCallLink__use_this_link_to_join_a_signal_call, CallLinks.url(viewModel.rootKeySnapshot))
)
)
}
@@ -324,7 +324,6 @@ private fun CallLinkDetailsScreenPreview() {
val callLink = remember {
val credentials = CallLinkCredentials(
byteArrayOf(1, 2, 3, 4),
byteArrayOf(0, 1, 2, 3),
byteArrayOf(3, 4, 5, 6)
)
CallLinkTable.CallLink(

View File

@@ -48,9 +48,6 @@ class CallLinkDetailsViewModel(
val rootKeySnapshot: ByteArray
get() = state.value.callLink?.credentials?.linkKeyBytes ?: error("Call link not loaded yet.")
val epochSnapshot: ByteArray?
get() = state.value.callLink?.credentials?.epochBytes
private val recipientSubject = BehaviorSubject.create<Recipient>()
val recipientSnapshot: Recipient?
get() = recipientSubject.value

View File

@@ -23,6 +23,8 @@ import io.reactivex.rxjava3.kotlin.subscribeBy
import kotlinx.coroutines.launch
import org.signal.core.ui.BottomSheetUtil
import org.signal.core.ui.compose.Snackbars
import org.signal.core.ui.getWindowSizeClass
import org.signal.core.ui.isSplitPane
import org.signal.core.util.DimensionUnit
import org.signal.core.util.concurrent.LifecycleDisposable
import org.signal.core.util.concurrent.addTo
@@ -62,8 +64,6 @@ import org.thoughtcrime.securesms.util.ViewUtil
import org.thoughtcrime.securesms.util.doAfterNextLayout
import org.thoughtcrime.securesms.util.fragments.requireListener
import org.thoughtcrime.securesms.util.visible
import org.thoughtcrime.securesms.window.getWindowSizeClass
import org.thoughtcrime.securesms.window.isSplitPane
import java.util.Objects
import org.signal.core.ui.R as CoreUiR

View File

@@ -36,12 +36,12 @@ import org.signal.core.ui.compose.AllDevicePreviews
import org.signal.core.ui.compose.Dialogs
import org.signal.core.ui.compose.DropdownMenus
import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.theme.SignalTheme
import org.thoughtcrime.securesms.PassphraseRequiredActivity
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.calls.new.NewCallUiState.CallType
import org.thoughtcrime.securesms.calls.new.NewCallUiState.UserMessage
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity
import org.thoughtcrime.securesms.compose.SignalTheme
import org.thoughtcrime.securesms.recipients.ui.RecipientLookupFailureMessage
import org.thoughtcrime.securesms.recipients.ui.RecipientPicker
import org.thoughtcrime.securesms.recipients.ui.RecipientPickerCallbacks

View File

@@ -15,8 +15,8 @@ import androidx.core.os.bundleOf
import androidx.fragment.app.setFragmentResult
import androidx.fragment.app.setFragmentResultListener
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import org.signal.core.ui.compose.ComposeBottomSheetDialogFragment
import org.signal.storageservice.protos.calls.quality.SubmitCallQualitySurveyRequest
import org.thoughtcrime.securesms.compose.ComposeBottomSheetDialogFragment
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.logsubmit.SubmitDebugLogActivity
import org.thoughtcrime.securesms.util.viewModel

View File

@@ -7,8 +7,8 @@ package org.thoughtcrime.securesms.calls.quality
import androidx.compose.runtime.Composable
import androidx.core.os.bundleOf
import org.signal.core.ui.compose.ComposeFullScreenDialogFragment
import org.signal.storageservice.protos.calls.quality.SubmitCallQualitySurveyRequest
import org.thoughtcrime.securesms.compose.ComposeFullScreenDialogFragment
class CallQualityDiagnosticsFragment : ComposeFullScreenDialogFragment() {

View File

@@ -13,7 +13,7 @@ import androidx.compose.runtime.remember
import androidx.core.os.bundleOf
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.setFragmentResult
import org.thoughtcrime.securesms.compose.ComposeFullScreenDialogFragment
import org.signal.core.ui.compose.ComposeFullScreenDialogFragment
/**
* Fragment which allows user to enter additional text to describe a call issue.

View File

@@ -15,7 +15,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.Util;
import org.signal.core.util.Util;
public class ArcProgressBar extends View {

View File

@@ -66,6 +66,7 @@ public class ComposeText extends EmojiEditText {
private MentionRendererDelegate mentionRendererDelegate;
private SpoilerRendererDelegate spoilerRendererDelegate;
private MentionValidatorWatcher mentionValidatorWatcher;
private MessageSendType lastMessageSendType;
@Nullable private InputPanel.MediaListener mediaListener;
@Nullable private CursorPositionChangedListener cursorPositionChangedListener;
@@ -221,6 +222,11 @@ public class ComposeText extends EmojiEditText {
}
public void setMessageSendType(MessageSendType messageSendType) {
if (messageSendType.equals(lastMessageSendType)) {
return;
}
lastMessageSendType = messageSendType;
int imeOptions = (getImeOptions() & ~EditorInfo.IME_MASK_ACTION) | EditorInfo.IME_ACTION_SEND;
int inputType = getInputType();

View File

@@ -33,7 +33,7 @@ import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.signal.core.ui.permissions.Permissions;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.MessageRecordUtil;

View File

@@ -16,6 +16,7 @@ import androidx.core.os.bundleOf
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import org.signal.core.ui.BottomSheetUtil
import org.signal.core.ui.FixedRoundedCornerBottomSheetDialogFragment
import org.signal.core.util.concurrent.LifecycleDisposable
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.R

View File

@@ -47,7 +47,10 @@ public abstract class FullScreenDialogFragment extends DialogFragment {
@Override
public void onResume() {
super.onResume();
WindowUtil.initializeScreenshotSecurity(requireContext(), requireDialog().getWindow());
if (getShowsDialog()) {
WindowUtil.initializeScreenshotSecurity(requireContext(), requireDialog().getWindow());
}
}
protected void onNavigateUp() {

View File

@@ -219,7 +219,7 @@ public class InputPanel extends ConstraintLayout
@NonNull SlideDeck attachments,
@NonNull QuoteModel.Type quoteType)
{
this.quoteView.setQuote(requestManager, id, author, body, false, attachments, null, quoteType, true);
this.quoteView.setQuote(requestManager, id, author, body, false, attachments, null, quoteType, true, null);
if (listener != null) {
this.quoteView.setOnClickListener(v -> listener.onQuoteClicked(id, author.getId()));
}

View File

@@ -31,7 +31,7 @@ import androidx.appcompat.widget.LinearLayoutCompat;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.ServiceUtil;
import org.thoughtcrime.securesms.util.Util;
import org.signal.core.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
import java.lang.reflect.Field;

View File

@@ -29,7 +29,7 @@ import org.thoughtcrime.securesms.linkpreview.LinkPreview;
import org.thoughtcrime.securesms.linkpreview.LinkPreviewRepository;
import org.thoughtcrime.securesms.mms.ImageSlide;
import org.thoughtcrime.securesms.mms.SlidesClickedListener;
import org.thoughtcrime.securesms.util.Util;
import org.signal.core.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
import java.text.DateFormat;
@@ -172,11 +172,11 @@ public class LinkPreviewView extends FrameLayout {
spinner.setVisibility(GONE);
noPreview.setVisibility(GONE);
CallLinks.CallLinkParseResult callLinkParseResult = CallLinks.isCallLink(linkPreview.getUrl()) ? CallLinks.parseUrl(linkPreview.getUrl()) : null;
CallLinkRootKey callLinkRootKey = CallLinks.isCallLink(linkPreview.getUrl()) ? CallLinks.parseUrl(linkPreview.getUrl()) : null;
if (!Util.isEmpty(linkPreview.getTitle())) {
title.setText(linkPreview.getTitle());
title.setVisibility(VISIBLE);
} else if (callLinkParseResult != null) {
} else if (callLinkRootKey != null) {
title.setText(R.string.Recipient_signal_call);
title.setVisibility(VISIBLE);
} else {
@@ -186,7 +186,7 @@ public class LinkPreviewView extends FrameLayout {
if (showDescription && !Util.isEmpty(linkPreview.getDescription())) {
description.setText(linkPreview.getDescription());
description.setVisibility(VISIBLE);
} else if (callLinkParseResult != null) {
} else if (callLinkRootKey != null) {
description.setText(R.string.LinkPreviewView__use_this_link_to_join_a_signal_call);
description.setVisibility(VISIBLE);
} else {
@@ -221,14 +221,14 @@ public class LinkPreviewView extends FrameLayout {
thumbnail.get().setImageResource(requestManager, new ImageSlide(linkPreview.getThumbnail().get()), type == TYPE_CONVERSATION && !scheduleMessageMode, false);
thumbnail.get().showSecondaryText(false);
thumbnail.get().setOutlineEnabled(true);
} else if (callLinkParseResult != null) {
} else if (callLinkRootKey != null) {
thumbnail.setVisibility(VISIBLE);
thumbnailState.applyState(thumbnail);
thumbnail.get().setImageDrawable(
requestManager,
new FallbackAvatarDrawable(
getContext(),
new FallbackAvatar.Resource.CallLink(AvatarColorHash.forCallLink(callLinkParseResult.getRootKey().getKeyBytes()))
new FallbackAvatar.Resource.CallLink(AvatarColorHash.forCallLink(callLinkRootKey.getKeyBytes()))
).circleCrop()
);
thumbnail.get().showSecondaryText(false);
@@ -272,7 +272,7 @@ public class LinkPreviewView extends FrameLayout {
thumbnailState.applyState(thumbnail);
}
private @StringRes static int getLinkPreviewErrorString(@Nullable LinkPreviewRepository.Error customError) {
private @StringRes static int getLinkPreviewErrorString(@Nullable LinkPreviewRepository.Error customError) {
return customError == LinkPreviewRepository.Error.GROUP_LINK_INACTIVE ? R.string.LinkPreviewView_this_group_link_is_not_active
: R.string.LinkPreviewView_no_link_preview_available;
}

View File

@@ -24,7 +24,7 @@ import androidx.core.content.ContextCompat;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.audio.AudioRecordingHandler;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.signal.core.ui.permissions.Permissions;
import org.thoughtcrime.securesms.util.ViewUtil;
public final class MicrophoneRecorderView extends FrameLayout implements View.OnTouchListener {

View File

@@ -13,6 +13,7 @@ import androidx.core.content.ContextCompat
import androidx.core.os.bundleOf
import androidx.fragment.app.FragmentManager
import org.signal.core.ui.BottomSheetUtil
import org.signal.core.ui.FixedRoundedCornerBottomSheetDialogFragment
import org.signal.core.util.concurrent.LifecycleDisposable
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.R

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