Compare commits

..

2 Commits

Author SHA1 Message Date
Greyson Parrelli
df2ac5cfd6 Bump version to 6.47.5 2024-02-23 15:40:45 -05:00
Greyson Parrelli
f12b7d8962 Attempt to prevent message retry loops. 2024-02-23 15:38:18 -05:00
2324 changed files with 66793 additions and 175715 deletions

View File

@@ -6,15 +6,4 @@ ij_kotlin_allow_trailing_comma_on_call_site = false
ij_kotlin_allow_trailing_comma = false
ktlint_code_style = intellij_idea
twitter_compose_allowed_composition_locals=LocalExtendedColors
ktlint_standard_class-naming = disabled
# below rules disabled during ktlint version migration because they were preexisting but should be corrected and re-enabled ASAP
ktlint_function_naming_ignore_when_annotated_with = Composable
ktlint_standard_property-naming = disabled
ktlint_standard_enum-wrapping = disabled
ktlint_standard_multiline-if-else = disabled
ktlint_standard_backing-property-naming = disabled
ktlint_standard_statement-wrapping = disabled
internal:ktlint-suppression = disabled
ktlint_standard_unnecessary-parentheses-before-trailing-lambda = disabled
ktlint_standard_value-parameter-comment = disabled
ktlint_standard_class-naming = disabled

View File

@@ -2,7 +2,7 @@
### First time contributor checklist
<!-- replace the empty checkboxes [ ] below with checked ones [x] accordingly -->
- [ ] I have read [how to contribute](https://github.com/signalapp/Signal-Android/blob/master/CONTRIBUTING.md) to this project
- [ ] I have signed the [Contributor License Agreement](https://signal.org/cla/)
- [ ] I have signed the [Contributor License Agreement](https://whispersystems.org/cla/)
### Contributor checklist
<!-- replace the empty checkboxes [ ] below with checked ones [x] accordingly -->

View File

@@ -32,7 +32,7 @@ jobs:
uses: gradle/wrapper-validation-action@v1
- name: Build with Gradle
run: ./gradlew qa
run: ./gradlew qa --parallel
- name: Archive reports for failed build
if: ${{ failure() }}

View File

@@ -38,7 +38,7 @@ jobs:
- name: Build with Gradle
if: steps.cache-base.outputs.cache-hit != 'true'
run: ./gradlew assemblePlayProdRelease
run: ./gradlew assemblePlayProdRelease --parallel
- name: Copy base apk
if: steps.cache-base.outputs.cache-hit != 'true'
@@ -50,7 +50,7 @@ jobs:
clean: 'false'
- name: Build with Gradle
run: ./gradlew assemblePlayProdRelease
run: ./gradlew assemblePlayProdRelease --parallel
- name: Copy PR apk
run: mv app/build/outputs/apk/playProd/release/*arm64*.apk diffuse-new.apk

2
.gitignore vendored
View File

@@ -29,4 +29,4 @@ jni/libspeex/.deps/
pkcs11.password
dev.keystore
maps.key
/local/
local/

3
.gitmodules vendored Normal file
View File

@@ -0,0 +1,3 @@
[submodule "libwebp"]
path = libwebp
url = https://github.com/webmproject/libwebp.git

View File

@@ -1,4 +1,4 @@
# Signal Android
# Signal Android
Signal is a simple, powerful, and secure messenger.
@@ -54,7 +54,7 @@ The form and manner of this distribution makes it eligible for export under the
## License
Copyright 2013-2024 Signal Messenger, LLC
Copyright 2013-2023 Signal
Licensed under the GNU AGPLv3: https://www.gnu.org/licenses/agpl-3.0.html

2
apntool/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
*.db
*.db.gz

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

106
apntool/apntool.py Normal file
View File

@@ -0,0 +1,106 @@
import sys
import re
import argparse
import sqlite3
import gzip
from progressbar import ProgressBar, Counter, Timer
from lxml import etree
parser = argparse.ArgumentParser(prog='apntool', description="""Process Android's apn xml files and drop them into an
easily queryable SQLite db. Tested up to version 9 of
their APN file.""")
parser.add_argument('-v', '--version', action='version', version='%(prog)s v1.1')
parser.add_argument('-i', '--input', help='the xml file to parse', default='apns.xml', required=False)
parser.add_argument('-o', '--output', help='the sqlite db output file', default='apns.db', required=False)
parser.add_argument('--quiet', help='do not show progress or verbose instructions', action='store_true', required=False)
parser.add_argument('--no-gzip', help="do not gzip after creation", action='store_true', required=False)
args = parser.parse_args()
def normalized(target):
o2_typo = re.compile(r"02\.co\.uk")
port_typo = re.compile(r"(\d+\.\d+\.\d+\.\d+)\.(\d+)")
leading_zeros = re.compile(r"(/|\.|^)0+(\d+)")
subbed = o2_typo.sub(r'o2.co.uk', target)
subbed = port_typo.sub(r'\1:\2', subbed)
subbed = leading_zeros.sub(r'\1\2', subbed)
return subbed
try:
connection = sqlite3.connect(args.output)
cursor = connection.cursor()
cursor.execute('SELECT SQLITE_VERSION()')
version = cursor.fetchone()
if not args.quiet:
print("SQLite version: %s" % version)
print("Opening %s" % args.input)
cursor.execute("PRAGMA legacy_file_format=ON")
cursor.execute("PRAGMA journal_mode=DELETE")
cursor.execute("PRAGMA page_size=32768")
cursor.execute("VACUUM")
cursor.execute("DROP TABLE IF EXISTS apns")
cursor.execute("""CREATE TABLE apns(_id INTEGER PRIMARY KEY, mccmnc TEXT, mcc TEXT, mnc TEXT, carrier TEXT,
apn TEXT, mmsc TEXT, port INTEGER, type TEXT, protocol TEXT, bearer TEXT, roaming_protocol TEXT,
carrier_enabled INTEGER, mmsproxy TEXT, mmsport INTEGER, proxy TEXT, mvno_match_data TEXT,
mvno_type TEXT, authtype INTEGER, user TEXT, password TEXT, server TEXT)""")
apns = etree.parse(args.input)
root = apns.getroot()
pbar = None
if not args.quiet:
pbar = ProgressBar(widgets=['Processed: ', Counter(), ' apns (', Timer(), ')'], maxval=len(list(root))).start()
count = 0
for apn in root.iter("apn"):
if apn.get("mmsc") is None:
continue
sqlvars = ["?" for x in apn.attrib.keys()] + ["?"]
mccmnc = "%s%s" % (apn.get("mcc"), apn.get("mnc"))
normalized_mmsc = normalized(apn.get("mmsc"))
if normalized_mmsc != apn.get("mmsc"):
print("normalize MMSC: %s => %s" % (apn.get("mmsc"), normalized_mmsc))
apn.set("mmsc", normalized_mmsc)
if not apn.get("mmsproxy") is None:
normalized_mmsproxy = normalized(apn.get("mmsproxy"))
if normalized_mmsproxy != apn.get("mmsproxy"):
print("normalize proxy: %s => %s" % (apn.get("mmsproxy"), normalized_mmsproxy))
apn.set("mmsproxy", normalized_mmsproxy)
values = [apn.get(attrib) for attrib in apn.attrib.keys()] + [mccmnc]
keys = apn.attrib.keys() + ["mccmnc"]
cursor.execute("SELECT 1 FROM apns WHERE mccmnc = ? AND apn = ?", [mccmnc, apn.get("apn")])
if cursor.fetchone() is None:
statement = "INSERT INTO apns (%s) VALUES (%s)" % (", ".join(keys), ", ".join(sqlvars))
cursor.execute(statement, values)
count += 1
if not args.quiet:
pbar.update(count)
if not args.quiet:
pbar.finish()
connection.commit()
print("Successfully written to %s" % args.output)
if not args.no_gzip:
gzipped_file = "%s.gz" % (args.output,)
with open(args.output, 'rb') as orig:
with gzip.open(gzipped_file, 'wb') as gzipped:
gzipped.writelines(orig)
print("Successfully gzipped to %s" % gzipped_file)
if not args.quiet:
print("\nTo include this in the distribution, copy it to the project's assets/databases/ directory.")
print("If you support API 10 or lower, you must use the gzipped version to avoid corruption.")
except sqlite3.Error as e:
if connection:
connection.rollback()
print("Error: %s" % e.args[0])
sys.exit(1)
finally:
if connection:
connection.close()

3
apntool/requirements.txt Normal file
View File

@@ -0,0 +1,3 @@
argparse>=1.2.1
lxml>=3.3.3
progressbar-latest>=2.4

View File

@@ -21,10 +21,17 @@ plugins {
apply(from = "static-ips.gradle.kts")
val canonicalVersionCode = 1450
val canonicalVersionName = "7.15.3"
val currentHotfixVersion = 0
val maxHotfixVersions = 100
val canonicalVersionCode = 1393
val canonicalVersionName = "6.47.5"
val postFixSize = 100
val abiPostFix: Map<String, Int> = mapOf(
"universal" to 5,
"armeabi-v7a" to 6,
"arm64-v8a" to 7,
"x86" to 8,
"x86_64" to 9
)
val keystores: Map<String, Properties?> = mapOf("debug" to loadKeystoreProperties("keystore.debug.properties"))
@@ -33,6 +40,8 @@ val selectableVariants = listOf(
"nightlyProdPerf",
"nightlyProdRelease",
"nightlyStagingRelease",
"nightlyPnpPerf",
"nightlyPnpRelease",
"playProdDebug",
"playProdSpinner",
"playProdCanary",
@@ -45,6 +54,8 @@ val selectableVariants = listOf(
"playStagingSpinner",
"playStagingPerf",
"playStagingInstrumentation",
"playPnpDebug",
"playPnpSpinner",
"playStagingRelease",
"websiteProdSpinner",
"websiteProdRelease"
@@ -72,7 +83,7 @@ wire {
}
ktlint {
version.set("1.2.1")
version.set("0.49.1")
}
android {
@@ -85,8 +96,6 @@ android {
useLibrary("org.apache.http.legacy")
testBuildType = "instrumentation"
android.bundle.language.enableSplit = false
kotlinOptions {
jvmTarget = signalKotlinJvmTarget
}
@@ -135,27 +144,9 @@ android {
targetCompatibility = signalJavaVersion
}
packaging {
jniLibs {
excludes += setOf(
"**/*.dylib",
"**/*.dll"
)
}
packagingOptions {
resources {
excludes += setOf(
"LICENSE.txt",
"LICENSE",
"NOTICE",
"asm-license.txt",
"META-INF/LICENSE",
"META-INF/LICENSE.md",
"META-INF/NOTICE",
"META-INF/LICENSE-notice.md",
"META-INF/proguard/androidx-annotations.pro",
"**/*.dylib",
"**/*.dll"
)
excludes += setOf("LICENSE.txt", "LICENSE", "NOTICE", "asm-license.txt", "META-INF/LICENSE", "META-INF/LICENSE.md", "META-INF/NOTICE", "META-INF/LICENSE-notice.md", "META-INF/proguard/androidx-annotations.pro", "libsignal_jni.dylib", "signal_jni.dll")
}
}
@@ -165,11 +156,11 @@ android {
}
composeOptions {
kotlinCompilerExtensionVersion = "1.5.4"
kotlinCompilerExtensionVersion = "1.4.4"
}
defaultConfig {
versionCode = (canonicalVersionCode * maxHotfixVersions) + currentHotfixVersion
versionCode = canonicalVersionCode * postFixSize
versionName = canonicalVersionName
minSdk = signalMinSdkVersion
@@ -191,6 +182,7 @@ android {
buildConfigField("String", "SIGNAL_CDN3_URL", "\"https://cdn3.signal.org\"")
buildConfigField("String", "SIGNAL_CDSI_URL", "\"https://cdsi.signal.org\"")
buildConfigField("String", "SIGNAL_SERVICE_STATUS_URL", "\"uptime.signal.org\"")
buildConfigField("String", "SIGNAL_KEY_BACKUP_URL", "\"https://api.backup.signal.org\"")
buildConfigField("String", "SIGNAL_SVR2_URL", "\"https://svr2.signal.org\"")
buildConfigField("String", "SIGNAL_SFU_URL", "\"https://sfu.voip.signal.org\"")
buildConfigField("String", "SIGNAL_STAGING_SFU_URL", "\"https://sfu.staging.voip.signal.org\"")
@@ -209,9 +201,10 @@ android {
buildConfigField("String[]", "SIGNAL_SVR2_IPS", rootProject.extra["svr2_ips"] as String)
buildConfigField("String", "SIGNAL_AGENT", "\"OWA\"")
buildConfigField("String", "CDSI_MRENCLAVE", "\"0f6fd79cdfdaa5b2e6337f534d3baf999318b0c462a7ac1f41297a3e4b424a57\"")
buildConfigField("String", "SVR2_MRENCLAVE_DEPRECATED", "\"6ee1042f9e20f880326686dd4ba50c25359f01e9f733eeba4382bca001d45094\"")
buildConfigField("String", "SVR2_MRENCLAVE", "\"a6622ad4656e1abcd0bc0ff17c229477747d2ded0495c4ebee7ed35c1789fa97\"")
buildConfigField("String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF\"")
buildConfigField("String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"AMhf5ywVwITZMsff/eCyudZx9JDmkkkbV6PInzG4p8x3VqVJSFiMvnvlEKWuRob/1eaIetR31IYeAbm0NdOuHH8Qi+Rexi1wLlpzIo1gstHWBfZzy1+qHRV5A4TqPp15YzBPm0WSggW6PbSn+F4lf57VCnHF7p8SvzAA2ZZJPYJURt8X7bbg+H3i+PEjH9DXItNEqs2sNcug37xZQDLm7X36nOoGPs54XsEGzPdEV+itQNGUFEjY6X9Uv+Acuks7NpyGvCoKxGwgKgE5XyJ+nNKlyHHOLb6N1NuHyBrZrgtY/JYJHRooo5CEqYKBqdFnmbTVGEkCvJKxLnjwKWf+fEPoWeQFj5ObDjcKMZf2Jm2Ae69x+ikU5gBXsRmoF94GXTLfN0/vLt98KDPnxwAQL9j5V1jGOY8jQl6MLxEs56cwXN0dqCnImzVH3TZT1cJ8SW1BRX6qIVxEzjsSGx3yxF3suAilPMqGRp4ffyopjMD1JXiKR2RwLKzizUe5e8XyGOy9fplzhw3jVzTRyUZTRSZKkMLWcQ/gv0E4aONNqs4P+NameAZYOD12qRkxosQQP5uux6B2nRyZ7sAV54DgFyLiRcq1FvwKw2EPQdk4HDoePrO/RNUbyNddnM/mMgj4FW65xCoT1LmjrIjsv/Ggdlx46ueczhMgtBunx1/w8k8V+l8LVZ8gAT6wkU5J+DPQalQguMg12Jzug3q4TbdHiGCmD9EunCwOmsLuLJkz6EcSYXtrlDEnAM+hicw7iergYLLlMXpfTdGxJCWJmP4zqUFeTTmsmhsjGBt7NiEB/9pFFEB3pSbf4iiUukw63Eo8Aqnf4iwob6X1QviCWuc8t0LUlT9vALgh/f2DPVOOmR0RW6bgRvc7DSF20V/omg+YBw==\"")
buildConfigField("String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"AMhf5ywVwITZMsff/eCyudZx9JDmkkkbV6PInzG4p8x3VqVJSFiMvnvlEKWuRob/1eaIetR31IYeAbm0NdOuHH8Qi+Rexi1wLlpzIo1gstHWBfZzy1+qHRV5A4TqPp15YzBPm0WSggW6PbSn+F4lf57VCnHF7p8SvzAA2ZZJPYJURt8X7bbg+H3i+PEjH9DXItNEqs2sNcug37xZQDLm7X36nOoGPs54XsEGzPdEV+itQNGUFEjY6X9Uv+Acuks7NpyGvCoKxGwgKgE5XyJ+nNKlyHHOLb6N1NuHyBrZrgtY/JYJHRooo5CEqYKBqdFnmbTVGEkCvJKxLnjwKWf+fEPoWeQFj5ObDjcKMZf2Jm2Ae69x+ikU5gBXsRmoF94GXTLfN0/vLt98KDPnxwAQL9j5V1jGOY8jQl6MLxEs56cwXN0dqCnImzVH3TZT1cJ8SW1BRX6qIVxEzjsSGx3yxF3suAilPMqGRp4ffyopjMD1JXiKR2RwLKzizUe5e8XyGOy9fplzhw3jVzTRyUZTRSZKkMLWcQ/gv0E4aONNqs4P+NameAZYOD12qRkxosQQP5uux6B2nRyZ7sAV54DgFyLiRcq1FvwKw2EPQdk4HDoePrO/RNUbyNddnM/mMgj4FW65xCoT1LmjrIjsv/Ggdlx46ueczhMgtBunx1/w8k8V+l8LVZ8gAT6wkU5J+DPQalQguMg12Jzug3q4TbdHiGCmD9EunCwOmsLuLJkz6EcSYXtrlDEnAM+hicw7iergYLLlMXpfTdGxJCWJmP4zqUFeTTmsmhsjGBt7NiEB/9pFFEB3pSbf4iiUukw63Eo8Aqnf4iwob6X1QviCWuc8t0I=\"")
buildConfigField("String", "GENERIC_SERVER_PUBLIC_PARAMS", "\"AByD873dTilmOSG0TjKrvpeaKEsUmIO8Vx9BeMmftwUs9v7ikPwM8P3OHyT0+X3EUMZrSe9VUp26Wai51Q9I8mdk0hX/yo7CeFGJyzoOqn8e/i4Ygbn5HoAyXJx5eXfIbqpc0bIxzju4H/HOQeOpt6h742qii5u/cbwOhFZCsMIbElZTaeU+BWMBQiZHIGHT5IE0qCordQKZ5iPZom0HeFa8Yq0ShuEyAl0WINBiY6xE3H/9WnvzXBbMuuk//eRxXgzO8ieCeK8FwQNxbfXqZm6Ro1cMhCOF3u7xoX83QhpN\"")
buildConfigField("String", "BACKUP_SERVER_PUBLIC_PARAMS", "\"AJwNSU55fsFCbgaxGRD11wO1juAs8Yr5GF8FPlGzzvdJJIKH5/4CC7ZJSOe3yL2vturVaRU2Cx0n751Vt8wkj1bozK3CBV1UokxV09GWf+hdVImLGjXGYLLhnI1J2TWEe7iWHyb553EEnRb5oxr9n3lUbNAJuRmFM7hrr0Al0F0wrDD4S8lo2mGaXe0MJCOM166F8oYRQqpFeEHfiLnxA1O8ZLh7vMdv4g9jI5phpRBTsJ5IjiJrWeP0zdIGHEssUeprDZ9OUJ14m0v61eYJMKsf59Bn+mAT2a7YfB+Don9O\"")
buildConfigField("String[]", "LANGUAGES", "new String[]{ ${languageList().map { "\"$it\"" }.joinToString(separator = ", ")} }")
@@ -220,8 +213,6 @@ android {
buildConfigField("String", "GIPHY_API_KEY", "\"3o6ZsYH6U6Eri53TXy\"")
buildConfigField("String", "SIGNAL_CAPTCHA_URL", "\"https://signalcaptchas.org/registration/generate.html\"")
buildConfigField("String", "RECAPTCHA_PROOF_URL", "\"https://signalcaptchas.org/challenge/generate.html\"")
buildConfigField("org.signal.libsignal.net.Network.Environment", "LIBSIGNAL_NET_ENV", "org.signal.libsignal.net.Network.Environment.PRODUCTION")
buildConfigField("int", "LIBSIGNAL_LOG_LEVEL", "org.signal.libsignal.protocol.logging.SignalProtocolLogger.INFO")
buildConfigField("String", "BUILD_DISTRIBUTION_TYPE", "\"unset\"")
buildConfigField("String", "BUILD_ENVIRONMENT_TYPE", "\"unset\"")
@@ -229,7 +220,6 @@ android {
buildConfigField("String", "BADGE_STATIC_ROOT", "\"https://updates2.signal.org/static/badges/\"")
buildConfigField("String", "STRIPE_PUBLISHABLE_KEY", "\"pk_live_6cmGZopuTsV8novGgJJW9JpC00vLIgtQ1D\"")
buildConfigField("boolean", "TRACING_ENABLED", "false")
buildConfigField("boolean", "MESSAGE_BACKUP_RESTORE_ENABLED", "false")
ndk {
abiFilters += listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
@@ -390,21 +380,29 @@ android {
buildConfigField("String", "SIGNAL_CDN2_URL", "\"https://cdn2-staging.signal.org\"")
buildConfigField("String", "SIGNAL_CDN3_URL", "\"https://cdn3-staging.signal.org\"")
buildConfigField("String", "SIGNAL_CDSI_URL", "\"https://cdsi.staging.signal.org\"")
buildConfigField("String", "SIGNAL_KEY_BACKUP_URL", "\"https://api-staging.backup.signal.org\"")
buildConfigField("String", "SIGNAL_SVR2_URL", "\"https://svr2.staging.signal.org\"")
buildConfigField("String", "SVR2_MRENCLAVE_DEPRECATED", "\"a8a261420a6bb9b61aa25bf8a79e8bd20d7652531feb3381cbffd446d270be95\"")
buildConfigField("String", "SVR2_MRENCLAVE", "\"acb1973aa0bbbd14b3b4e06f145497d948fd4a98efc500fcce363b3b743ec482\"")
buildConfigField("String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx\"")
buildConfigField("String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"ABSY21VckQcbSXVNCGRYJcfWHiAMZmpTtTELcDmxgdFbtp/bWsSxZdMKzfCp8rvIs8ocCU3B37fT3r4Mi5qAemeGeR2X+/YmOGR5ofui7tD5mDQfstAI9i+4WpMtIe8KC3wU5w3Inq3uNWVmoGtpKndsNfwJrCg0Hd9zmObhypUnSkfYn2ooMOOnBpfdanRtrvetZUayDMSC5iSRcXKpdlukrpzzsCIvEwjwQlJYVPOQPj4V0F4UXXBdHSLK05uoPBCQG8G9rYIGedYsClJXnbrgGYG3eMTG5hnx4X4ntARBgELuMWWUEEfSK0mjXg+/2lPmWcTZWR9nkqgQQP0tbzuiPm74H2wMO4u1Wafe+UwyIlIT9L7KLS19Aw8r4sPrXZSSsOZ6s7M1+rTJN0bI5CKY2PX29y5Ok3jSWufIKcgKOnWoP67d5b2du2ZVJjpjfibNIHbT/cegy/sBLoFwtHogVYUewANUAXIaMPyCLRArsKhfJ5wBtTminG/PAvuBdJ70Z/bXVPf8TVsR292zQ65xwvWTejROW6AZX6aqucUjlENAErBme1YHmOSpU6tr6doJ66dPzVAWIanmO/5mgjNEDeK7DDqQdB1xd03HT2Qs2TxY3kCK8aAb/0iM0HQiXjxZ9HIgYhbtvGEnDKW5ILSUydqH/KBhW4Pb0jZWnqN/YgbWDKeJxnDbYcUob5ZY5Lt5ZCMKuaGUvCJRrCtuugSMaqjowCGRempsDdJEt+cMaalhZ6gczklJB/IbdwENW9KeVFPoFNFzhxWUIS5ML9riVYhAtE6JE5jX0xiHNVIIPthb458cfA8daR0nYfYAUKogQArm0iBezOO+mPk5vCNWI+wwkyFCqNDXz/qxl1gAntuCJtSfq9OC3NkdhQlgYQ==\"")
buildConfigField("String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"ABSY21VckQcbSXVNCGRYJcfWHiAMZmpTtTELcDmxgdFbtp/bWsSxZdMKzfCp8rvIs8ocCU3B37fT3r4Mi5qAemeGeR2X+/YmOGR5ofui7tD5mDQfstAI9i+4WpMtIe8KC3wU5w3Inq3uNWVmoGtpKndsNfwJrCg0Hd9zmObhypUnSkfYn2ooMOOnBpfdanRtrvetZUayDMSC5iSRcXKpdlukrpzzsCIvEwjwQlJYVPOQPj4V0F4UXXBdHSLK05uoPBCQG8G9rYIGedYsClJXnbrgGYG3eMTG5hnx4X4ntARBgELuMWWUEEfSK0mjXg+/2lPmWcTZWR9nkqgQQP0tbzuiPm74H2wMO4u1Wafe+UwyIlIT9L7KLS19Aw8r4sPrXZSSsOZ6s7M1+rTJN0bI5CKY2PX29y5Ok3jSWufIKcgKOnWoP67d5b2du2ZVJjpjfibNIHbT/cegy/sBLoFwtHogVYUewANUAXIaMPyCLRArsKhfJ5wBtTminG/PAvuBdJ70Z/bXVPf8TVsR292zQ65xwvWTejROW6AZX6aqucUjlENAErBme1YHmOSpU6tr6doJ66dPzVAWIanmO/5mgjNEDeK7DDqQdB1xd03HT2Qs2TxY3kCK8aAb/0iM0HQiXjxZ9HIgYhbtvGEnDKW5ILSUydqH/KBhW4Pb0jZWnqN/YgbWDKeJxnDbYcUob5ZY5Lt5ZCMKuaGUvCJRrCtuugSMaqjowCGRempsDdJEt+cMaalhZ6gczklJB/IbdwENW9KeVFPoFNFzhxWUIS5ML9riVYhAtE6JE5jX0xiHNVIIPthb458cfA8daR0nYfYAUKogQArm0iBezOO+mPk5vCM=\"")
buildConfigField("String", "GENERIC_SERVER_PUBLIC_PARAMS", "\"AHILOIrFPXX9laLbalbA9+L1CXpSbM/bTJXZGZiuyK1JaI6dK5FHHWL6tWxmHKYAZTSYmElmJ5z2A5YcirjO/yfoemE03FItyaf8W1fE4p14hzb5qnrmfXUSiAIVrhaXVwIwSzH6RL/+EO8jFIjJ/YfExfJ8aBl48CKHgu1+A6kWynhttonvWWx6h7924mIzW0Czj2ROuh4LwQyZypex4GuOPW8sgIT21KNZaafgg+KbV7XM1x1tF3XA17B4uGUaDbDw2O+nR1+U5p6qHPzmJ7ggFjSN6Utu+35dS1sS0P9N\"")
buildConfigField("String", "BACKUP_SERVER_PUBLIC_PARAMS", "\"AHYrGb9IfugAAJiPKp+mdXUx+OL9zBolPYHYQz6GI1gWjpEu5me3zVNSvmYY4zWboZHif+HG1sDHSuvwFd0QszSwuSF4X4kRP3fJREdTZ5MCR0n55zUppTwfHRW2S4sdQ0JGz7YDQIJCufYSKh0pGNEHL6hv79Agrdnr4momr3oXdnkpVBIp3HWAQ6IbXQVSG18X36GaicI1vdT0UFmTwU2KTneluC2eyL9c5ff8PcmiS+YcLzh0OKYQXB5ZfQ06d6DiINvDQLy75zcfUOniLAj0lGJiHxGczin/RXisKSR8\"")
buildConfigField("String", "MOBILE_COIN_ENVIRONMENT", "\"testnet\"")
buildConfigField("String", "SIGNAL_CAPTCHA_URL", "\"https://signalcaptchas.org/staging/registration/generate.html\"")
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("String", "BUILD_ENVIRONMENT_TYPE", "\"Staging\"")
buildConfigField("String", "STRIPE_PUBLISHABLE_KEY", "\"pk_test_sngOd8FnXNkpce9nPXawKrJD00kIDngZkD\"")
buildConfigField("boolean", "MESSAGE_BACKUP_RESTORE_ENABLED", "true")
}
create("pnp") {
dimension = "environment"
initWith(getByName("staging"))
applicationIdSuffix = ".pnp"
buildConfigField("String", "BUILD_ENVIRONMENT_TYPE", "\"Pnp\"")
}
}
@@ -420,6 +418,7 @@ android {
.map { it as com.android.build.gradle.internal.api.ApkVariantOutputImpl }
.forEach { output ->
if (output.baseName.contains("nightly")) {
output.versionCodeOverride = canonicalVersionCode * postFixSize + 5
var tag = getCurrentGitTag()
if (!tag.isNullOrEmpty()) {
if (tag.startsWith("v")) {
@@ -433,9 +432,14 @@ android {
} else {
output.outputFileName = output.outputFileName.replace(".apk", "-$versionName.apk")
if (currentHotfixVersion >= maxHotfixVersions) {
throw AssertionError("Hotfix version is too large!")
val abiName: String = output.getFilter("ABI") ?: "universal"
val postFix: Int = abiPostFix[abiName]!!
if (postFix >= postFixSize) {
throw AssertionError("postFix is too large")
}
output.versionCodeOverride = canonicalVersionCode * postFixSize + postFix
}
}
}
@@ -444,12 +448,6 @@ android {
beforeVariants { variant ->
variant.enable = variant.name in selectableVariants
}
onVariants { variant ->
// Include the test-only library on debug builds.
if (variant.buildType != "instrumentation") {
variant.packaging.jniLibs.excludes.add("**/libsignal_jni_testing.so")
}
}
}
val releaseDir = "$projectDir/src/release/java"
@@ -511,11 +509,9 @@ dependencies {
implementation(libs.androidx.lifecycle.viewmodel.savedstate)
implementation(libs.androidx.lifecycle.common.java8)
implementation(libs.androidx.lifecycle.reactivestreams.ktx)
implementation(libs.androidx.lifecycle.runtime.compose)
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.camera.core)
implementation(libs.androidx.camera.camera2)
implementation(libs.androidx.camera.extensions)
implementation(libs.androidx.camera.lifecycle)
implementation(libs.androidx.camera.view)
implementation(libs.androidx.concurrent.futures)
@@ -525,7 +521,6 @@ dependencies {
implementation(libs.androidx.profileinstaller)
implementation(libs.androidx.asynclayoutinflater)
implementation(libs.androidx.asynclayoutinflater.appcompat)
implementation(libs.androidx.emoji2)
implementation(libs.firebase.messaging) {
exclude(group = "com.google.firebase", module = "firebase-core")
exclude(group = "com.google.firebase", module = "firebase-analytics")
@@ -555,9 +550,12 @@ dependencies {
implementation(libs.android.tooltips) {
exclude(group = "com.android.support", module = "appcompat-v7")
}
implementation(libs.android.smsmms) {
exclude(group = "com.squareup.okhttp", module = "okhttp")
exclude(group = "com.squareup.okhttp", module = "okhttp-urlconnection")
}
implementation(libs.stream)
implementation(libs.lottie)
implementation(libs.lottie.compose)
implementation(libs.signal.android.database.sqlcipher)
implementation(libs.androidx.sqlite)
implementation(libs.google.ez.vcard) {
@@ -569,16 +567,11 @@ dependencies {
implementation(libs.accompanist.permissions)
implementation(libs.kotlin.stdlib.jdk8)
implementation(libs.kotlin.reflect)
implementation(libs.kotlinx.coroutines.play.services)
implementation(libs.kotlinx.coroutines.rx3)
implementation(libs.jackson.module.kotlin)
implementation(libs.rxjava3.rxandroid)
implementation(libs.rxjava3.rxkotlin)
implementation(libs.rxdogtag)
"playImplementation"(project(":billing"))
"nightlyImplementation"(project(":billing"))
"spinnerImplementation"(project(":spinner"))
"canaryImplementation"(libs.square.leakcanary)
@@ -621,7 +614,6 @@ dependencies {
androidTestImplementation(testLibs.mockito.kotlin)
androidTestImplementation(testLibs.mockk.android)
androidTestImplementation(testLibs.square.okhttp.mockserver)
androidTestImplementation(testLibs.diff.utils)
androidTestUtil(testLibs.androidx.test.orchestrator)
}
@@ -736,8 +728,7 @@ fun Project.languageList(): List<String> {
.map { valuesFolderName -> valuesFolderName.replace("values-", "") }
.filter { valuesFolderName -> valuesFolderName != "values" }
.map { languageCode -> languageCode.replace("-r", "_") }
.distinct()
.sorted() + "en"
.distinct() + "en"
}
fun String.capitalize(): String {

View File

@@ -2,9 +2,7 @@
-dontobfuscate
-keepattributes SourceFile,LineNumberTable
-keep class org.whispersystems.** { *; }
-keep class org.signal.libsignal.net.** { *; }
-keep class org.signal.libsignal.protocol.** { *; }
-keep class org.signal.libsignal.usernames.** { *; }
-keep class org.thoughtcrime.securesms.** { *; }
-keep class org.signal.donations.json.** { *; }
-keepclassmembers class ** {
@@ -16,10 +14,6 @@
-keep class androidx.window.** { *; }
-keepclassmembers class * extends androidx.constraintlayout.motion.widget.Key {
public <init>();
}
# AGP generated dont warns
-dontwarn com.android.org.conscrypt.SSLParametersImpl
-dontwarn org.apache.harmony.xnet.provider.jsse.SSLParametersImpl

File diff suppressed because one or more lines are too long

View File

@@ -5,7 +5,7 @@ import org.signal.core.util.logging.AndroidLogger
import org.signal.core.util.logging.Log
import org.signal.libsignal.protocol.logging.SignalProtocolLoggerProvider
import org.thoughtcrime.securesms.database.LogDatabase
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.dependencies.ApplicationDependencyProvider
import org.thoughtcrime.securesms.dependencies.InstrumentationApplicationDependencyProvider
import org.thoughtcrime.securesms.logging.CustomSignalProtocolLogger
@@ -21,8 +21,8 @@ class SignalInstrumentationApplicationContext : ApplicationContext() {
override fun initializeAppDependencies() {
val default = ApplicationDependencyProvider(this)
AppDependencies.init(this, InstrumentationApplicationDependencyProvider(this, default))
AppDependencies.deadlockDetector.start()
ApplicationDependencies.init(this, InstrumentationApplicationDependencyProvider(this, default))
ApplicationDependencies.getDeadlockDetector().start()
}
override fun initializeLogging() {
@@ -35,18 +35,4 @@ class SignalInstrumentationApplicationContext : ApplicationContext() {
LogDatabase.getInstance(this).logs.trimToSize()
}
}
override fun beginJobLoop() = Unit
/**
* Some of the jobs can interfere with some of the instrumentation tests.
*
* For example, we may try to create a release channel recipient while doing
* an import/backup test.
*
* This can be used to start the job loop if needed for tests that rely on it.
*/
fun beginJobLoopForTests() {
super.beginJobLoop()
}
}

View File

@@ -1,287 +0,0 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.github.difflib.DiffUtils
import com.github.difflib.UnifiedDiffUtils
import junit.framework.Assert.assertTrue
import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.signal.core.util.Base64
import org.signal.core.util.SqlUtil
import org.signal.core.util.logging.Log
import org.signal.core.util.readFully
import org.signal.libsignal.messagebackup.ComparableBackup
import org.signal.libsignal.messagebackup.MessageBackup
import org.signal.libsignal.zkgroup.profiles.ProfileKey
import org.thoughtcrime.securesms.backup.v2.proto.Frame
import org.thoughtcrime.securesms.backup.v2.stream.PlainTextBackupReader
import org.thoughtcrime.securesms.database.DistributionListTables
import org.thoughtcrime.securesms.database.KeyValueDatabase
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.recipients.RecipientId
import org.whispersystems.signalservice.api.kbs.MasterKey
import org.whispersystems.signalservice.api.push.ServiceId
import java.io.ByteArrayInputStream
import java.util.UUID
@RunWith(AndroidJUnit4::class)
class ArchiveImportExportTests {
companion object {
const val TAG = "ImportExport"
const val TESTS_FOLDER = "backupTests"
val SELF_ACI = ServiceId.ACI.from(UUID(100, 100))
val SELF_PNI = ServiceId.PNI.from(UUID(101, 101))
val SELF_E164 = "+10000000000"
val SELF_PROFILE_KEY: ByteArray = Base64.decode("YQKRq+3DQklInaOaMcmlzZnN0m/1hzLiaONX7gB12dg=")
val MASTER_KEY = Base64.decode("sHuBMP4ToZk4tcNU+S8eBUeCt8Am5EZnvuqTBJIR4Do")
}
@Test
fun all() {
runTests()
}
@Ignore("Just for debugging")
@Test
fun accountData() {
runTests { it.startsWith("account_data_") }
}
@Ignore("Just for debugging")
@Test
fun recipientContacts() {
runTests { it.startsWith("recipient_contacts_") }
}
@Ignore("Just for debugging")
@Test
fun recipientDistributionLists() {
runTests { it.startsWith("recipient_distribution_list_") }
}
@Ignore("Just for debugging")
@Test
fun recipientGroups() {
runTests { it.startsWith("recipient_groups_") }
}
@Ignore("Just for debugging")
@Test
fun chatStandardMessageTextOnly() {
runTests { it.startsWith("chat_standard_message_text_only_") }
}
@Ignore("Just for debugging")
@Test
fun chatStandardMessageFormattedText() {
runTests { it.startsWith("chat_standard_message_formatted_text_") }
}
@Ignore("Just for debugging")
@Test
fun chatStandardMessageLongText() {
runTests { it.startsWith("chat_standard_message_long_text_") }
}
@Ignore("Just for debugging")
@Test
fun chatStandardMessageStandardAttachments() {
runTests { it.startsWith("chat_standard_message_standard_attachments_") }
}
@Ignore("Just for debugging")
@Test
fun chatStandardMessageSpecialAttachments() {
runTests { it.startsWith("chat_standard_message_special_attachments_") }
}
@Ignore("Just for debugging")
@Test
fun chatSimpleUpdates() {
runTests { it.startsWith("chat_simple_updates_") }
}
@Ignore("Just for debugging")
@Test
fun chatContactMessage() {
runTests { it.startsWith("chat_contact_message_") }
}
private fun runTests(predicate: (String) -> Boolean = { true }) {
val testFiles = InstrumentationRegistry.getInstrumentation().context.resources.assets.list(TESTS_FOLDER)!!.filter(predicate)
val results: MutableList<TestResult> = mutableListOf()
Log.d(TAG, "About to run ${testFiles.size} tests.")
for (filename in testFiles) {
Log.d(TAG, "> $filename")
val startTime = System.currentTimeMillis()
val result = test(filename)
results += result
if (result is TestResult.Success) {
Log.d(TAG, " \uD83D\uDFE2 Passed in ${System.currentTimeMillis() - startTime} ms")
} else {
Log.d(TAG, " \uD83D\uDD34 Failed in ${System.currentTimeMillis() - startTime} ms")
}
}
results
.filterIsInstance<TestResult.Failure>()
.forEach {
Log.e(TAG, "Failure: ${it.name}\n${it.message}")
Log.e(TAG, "----------------------------------")
Log.e(TAG, "----------------------------------")
Log.e(TAG, "----------------------------------")
}
if (results.any { it is TestResult.Failure }) {
val successCount = results.count { it is TestResult.Success }
val failingTestNames = results.filterIsInstance<TestResult.Failure>().joinToString(separator = "\n") { " \uD83D\uDD34 ${it.name}" }
val message = "Some tests failed! Only $successCount/${results.size} passed. Failure details are above. Failing tests:\n$failingTestNames"
Log.d(TAG, message)
throw AssertionError(message)
} else {
Log.d(TAG, "All ${results.size} tests passed!")
}
}
private fun test(filename: String): TestResult {
resetAllData()
val inputFileBytes: ByteArray = InstrumentationRegistry.getInstrumentation().context.resources.assets.open("$TESTS_FOLDER/$filename").readFully(true)
val importResult = import(inputFileBytes)
assertTrue(importResult is ImportResult.Success)
val success = importResult as ImportResult.Success
val generatedBackupData = BackupRepository.debugExport(plaintext = true, currentTime = success.backupTime)
checkEquivalent(filename, inputFileBytes, generatedBackupData)?.let { return it }
return TestResult.Success(filename)
}
private fun resetAllData() {
// Need to delete these first to prevent foreign key crash
SignalDatabase.rawDatabase.execSQL("DELETE FROM ${DistributionListTables.ListTable.TABLE_NAME}")
SignalDatabase.rawDatabase.execSQL("DELETE FROM ${DistributionListTables.MembershipTable.TABLE_NAME}")
SqlUtil.getAllTables(SignalDatabase.rawDatabase)
.filterNot { it.contains("sqlite") || it.contains("fts") || it.startsWith("emoji_search_") } // If we delete these we'll corrupt the DB
.sorted()
.forEach { table ->
SignalDatabase.rawDatabase.execSQL("DELETE FROM $table")
SqlUtil.resetAutoIncrementValue(SignalDatabase.rawDatabase, table)
}
AppDependencies.recipientCache.clear()
AppDependencies.recipientCache.clearSelf()
RecipientId.clearCache()
KeyValueDatabase.getInstance(AppDependencies.application).clear()
SignalStore.resetCache()
SignalStore.svr.setMasterKey(MasterKey(MASTER_KEY), "1234")
SignalStore.account.setE164(SELF_E164)
SignalStore.account.setAci(SELF_ACI)
SignalStore.account.setPni(SELF_PNI)
SignalStore.account.generateAciIdentityKeyIfNecessary()
SignalStore.account.generatePniIdentityKeyIfNecessary()
SignalStore.backup.backupTier = MessageBackupTier.PAID
}
private fun import(importData: ByteArray): ImportResult {
return BackupRepository.import(
length = importData.size.toLong(),
inputStreamFactory = { ByteArrayInputStream(importData) },
selfData = BackupRepository.SelfData(SELF_ACI, SELF_PNI, SELF_E164, ProfileKey(SELF_PROFILE_KEY)),
plaintext = true
)
}
private fun assertPassesValidator(testName: String, generatedBackupData: ByteArray): TestResult.Failure? {
try {
BackupRepository.validate(
length = generatedBackupData.size.toLong(),
inputStreamFactory = { ByteArrayInputStream(generatedBackupData) },
selfData = BackupRepository.SelfData(SELF_ACI, SELF_PNI, SELF_E164, ProfileKey(SELF_PROFILE_KEY))
)
} catch (e: Exception) {
return TestResult.Failure(testName, "Generated backup failed validation: ${e.message}")
}
return null
}
private fun checkEquivalent(testName: String, import: ByteArray, export: ByteArray): TestResult.Failure? {
val importComparable = try {
ComparableBackup.readUnencrypted(MessageBackup.Purpose.REMOTE_BACKUP, import.inputStream(), import.size.toLong())
} catch (e: Exception) {
return TestResult.Failure(testName, "Imported backup hit a validation error: ${e.message}")
}
val exportComparable = try {
ComparableBackup.readUnencrypted(MessageBackup.Purpose.REMOTE_BACKUP, export.inputStream(), import.size.toLong())
} catch (e: Exception) {
return TestResult.Failure(testName, "Exported backup hit a validation error: ${e.message}")
}
if (importComparable.unknownFieldMessages.isNotEmpty()) {
return TestResult.Failure(testName, "Imported backup contains unknown fields: ${importComparable.unknownFieldMessages}")
}
if (exportComparable.unknownFieldMessages.isNotEmpty()) {
return TestResult.Failure(testName, "Imported backup contains unknown fields: ${importComparable.unknownFieldMessages}")
}
val canonicalImport = importComparable.comparableString
val canonicalExport = exportComparable.comparableString
if (canonicalImport != canonicalExport) {
val importLines = canonicalImport.lines()
val exportLines = canonicalExport.lines()
val patch = DiffUtils.diff(importLines, exportLines)
val diff = UnifiedDiffUtils.generateUnifiedDiff("Import", "Export", importLines, patch, 3).joinToString(separator = "\n")
val importFrames = import.toFrames()
val exportFrames = export.toFrames()
val importGroupFramesByMasterKey = importFrames.mapNotNull { it.recipient?.group }.associateBy { it.masterKey }
val exportGroupFramesByMasterKey = exportFrames.mapNotNull { it.recipient?.group }.associateBy { it.masterKey }
val groupErrorMessage = StringBuilder()
for ((importKey, importValue) in importGroupFramesByMasterKey) {
if (exportGroupFramesByMasterKey[importKey]?.let { it.snapshot != importValue.snapshot } == true) {
groupErrorMessage.append("[$importKey] Snapshot mismatch.\nImport:\n${importValue}\n\nExport:\n${exportGroupFramesByMasterKey[importKey]}\n\n")
}
}
return TestResult.Failure(testName, "Imported backup does not match exported backup. Diff:\n$diff\n$groupErrorMessage")
}
return null
}
fun ByteArray.toFrames(): List<Frame> {
return PlainTextBackupReader(this.inputStream(), this.size.toLong()).use { it.asSequence().toList() }
}
private sealed class TestResult(val name: String) {
class Success(name: String) : TestResult(name)
class Failure(name: String, val message: String) : TestResult(name)
}
}

View File

@@ -0,0 +1,675 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2
import android.content.ContentValues
import android.database.Cursor
import androidx.core.content.contentValuesOf
import net.zetetic.database.sqlcipher.SQLiteDatabase
import org.junit.Before
import org.junit.Test
import org.signal.core.util.Hex
import org.signal.core.util.SqlUtil
import org.signal.core.util.insertInto
import org.signal.core.util.readToList
import org.signal.core.util.readToSingleObject
import org.signal.core.util.requireBlob
import org.signal.core.util.requireLong
import org.signal.core.util.requireString
import org.signal.core.util.select
import org.signal.core.util.toInt
import org.signal.core.util.withinTransaction
import org.signal.libsignal.zkgroup.profiles.ProfileKey
import org.thoughtcrime.securesms.backup.v2.database.clearAllDataForBackupRestore
import org.thoughtcrime.securesms.database.CallTable
import org.thoughtcrime.securesms.database.EmojiSearchTable
import org.thoughtcrime.securesms.database.MessageTable
import org.thoughtcrime.securesms.database.MessageTypes
import org.thoughtcrime.securesms.database.RecipientTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.mms.QuoteModel
import org.thoughtcrime.securesms.profiles.ProfileName
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.subscription.Subscriber
import org.thoughtcrime.securesms.testing.assertIs
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.whispersystems.signalservice.api.push.ServiceId.ACI
import org.whispersystems.signalservice.api.push.ServiceId.PNI
import org.whispersystems.signalservice.api.subscriptions.SubscriberId
import java.io.ByteArrayInputStream
import java.util.UUID
import kotlin.random.Random
typealias DatabaseData = Map<String, List<Map<String, Any?>>>
class BackupTest {
companion object {
val SELF_ACI = ACI.from(UUID.fromString("77770000-b477-4f35-a824-d92987a63641"))
val SELF_PNI = PNI.from(UUID.fromString("77771111-b014-41fb-bf73-05cb2ec52910"))
const val SELF_E164 = "+10000000000"
val SELF_PROFILE_KEY = ProfileKey(Random.nextBytes(32))
val ALICE_ACI = ACI.from(UUID.fromString("aaaa0000-5a76-47fa-a98a-7e72c948a82e"))
val ALICE_PNI = PNI.from(UUID.fromString("aaaa1111-c960-4f6c-8385-671ad2ffb999"))
val ALICE_E164 = "+12222222222"
/** Columns that we don't need to check equality of */
private val IGNORED_COLUMNS: Map<String, Set<String>> = mapOf(
RecipientTable.TABLE_NAME to setOf(RecipientTable.STORAGE_SERVICE_ID),
MessageTable.TABLE_NAME to setOf(MessageTable.FROM_DEVICE_ID)
)
/** Tables we don't need to check equality of */
private val IGNORED_TABLES: Set<String> = setOf(
EmojiSearchTable.TABLE_NAME,
"sqlite_sequence",
"message_fts_data",
"message_fts_idx",
"message_fts_docsize"
)
}
@Before
fun setup() {
SignalStore.account().setE164(SELF_E164)
SignalStore.account().setAci(SELF_ACI)
SignalStore.account().setPni(SELF_PNI)
SignalStore.account().generateAciIdentityKeyIfNecessary()
SignalStore.account().generatePniIdentityKeyIfNecessary()
}
@Test
fun emptyDatabase() {
backupTest { }
}
@Test
fun noteToSelf() {
backupTest {
individualChat(aci = SELF_ACI, givenName = "Note to Self") {
standardMessage(outgoing = true, body = "A")
standardMessage(outgoing = true, body = "B")
standardMessage(outgoing = true, body = "C")
}
}
}
@Test
fun individualChat() {
backupTest {
individualChat(aci = ALICE_ACI, givenName = "Alice") {
val m1 = standardMessage(outgoing = true, body = "Outgoing 1")
val m2 = standardMessage(outgoing = false, body = "Incoming 1", read = true)
standardMessage(outgoing = true, body = "Outgoing 2", quotes = m2)
standardMessage(outgoing = false, body = "Incoming 2", quotes = m1, quoteTargetMissing = true, read = false)
standardMessage(outgoing = true, body = "Outgoing 3, with mention", randomMention = true)
standardMessage(outgoing = false, body = "Incoming 3, with style", read = false, randomStyling = true)
remoteDeletedMessage(outgoing = true)
remoteDeletedMessage(outgoing = false)
}
}
}
@Test
fun individualRecipients() {
backupTest {
// Comprehensive example
individualRecipient(
aci = ALICE_ACI,
pni = ALICE_PNI,
e164 = ALICE_E164,
givenName = "Alice",
familyName = "Smith",
username = "alice.99",
hidden = false,
registeredState = RecipientTable.RegisteredState.REGISTERED,
profileKey = ProfileKey(Random.nextBytes(32)),
profileSharing = true,
hideStory = false
)
// Trying to get coverage of all the various values
individualRecipient(aci = ACI.from(UUID.randomUUID()), registeredState = RecipientTable.RegisteredState.NOT_REGISTERED)
individualRecipient(aci = ACI.from(UUID.randomUUID()), registeredState = RecipientTable.RegisteredState.UNKNOWN)
individualRecipient(pni = PNI.from(UUID.randomUUID()))
individualRecipient(e164 = "+15551234567")
individualRecipient(aci = ACI.from(UUID.randomUUID()), givenName = "Bob")
individualRecipient(aci = ACI.from(UUID.randomUUID()), familyName = "Smith")
individualRecipient(aci = ACI.from(UUID.randomUUID()), profileSharing = false)
individualRecipient(aci = ACI.from(UUID.randomUUID()), hideStory = true)
individualRecipient(aci = ACI.from(UUID.randomUUID()), hidden = true)
}
}
@Test
fun individualCallLogs() {
backupTest {
val aliceId = individualRecipient(
aci = ALICE_ACI,
pni = ALICE_PNI,
e164 = ALICE_E164,
givenName = "Alice",
familyName = "Smith",
username = "alice.99",
hidden = false,
registeredState = RecipientTable.RegisteredState.REGISTERED,
profileKey = ProfileKey(Random.nextBytes(32)),
profileSharing = true,
hideStory = false
)
insertOneToOneCallVariations(1, 1, aliceId)
}
}
private fun insertOneToOneCallVariations(callId: Long, timestamp: Long, id: RecipientId): Long {
val directions = arrayOf(CallTable.Direction.INCOMING, CallTable.Direction.OUTGOING)
val callTypes = arrayOf(CallTable.Type.AUDIO_CALL, CallTable.Type.VIDEO_CALL)
val events = arrayOf(
CallTable.Event.MISSED,
CallTable.Event.OUTGOING_RING,
CallTable.Event.ONGOING,
CallTable.Event.ACCEPTED,
CallTable.Event.NOT_ACCEPTED
)
var callTimestamp: Long = timestamp
var currentCallId = callId
for (direction in directions) {
for (event in events) {
for (type in callTypes) {
insertOneToOneCall(callId = currentCallId, callTimestamp, id, type, direction, event)
callTimestamp++
currentCallId++
}
}
}
return currentCallId
}
private fun insertOneToOneCall(callId: Long, timestamp: Long, peer: RecipientId, type: CallTable.Type, direction: CallTable.Direction, event: CallTable.Event) {
val messageType: Long = CallTable.Call.getMessageType(type, direction, event)
SignalDatabase.rawDatabase.withinTransaction {
val recipient = Recipient.resolved(peer)
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(recipient)
val outgoing = direction == CallTable.Direction.OUTGOING
val messageValues = contentValuesOf(
MessageTable.FROM_RECIPIENT_ID to if (outgoing) Recipient.self().id.serialize() else peer.serialize(),
MessageTable.FROM_DEVICE_ID to 1,
MessageTable.TO_RECIPIENT_ID to if (outgoing) peer.serialize() else Recipient.self().id.serialize(),
MessageTable.DATE_RECEIVED to timestamp,
MessageTable.DATE_SENT to timestamp,
MessageTable.READ to 1,
MessageTable.TYPE to messageType,
MessageTable.THREAD_ID to threadId
)
val messageId = SignalDatabase.rawDatabase.insert(MessageTable.TABLE_NAME, null, messageValues)
val values = contentValuesOf(
CallTable.CALL_ID to callId,
CallTable.MESSAGE_ID to messageId,
CallTable.PEER to peer.serialize(),
CallTable.TYPE to CallTable.Type.serialize(type),
CallTable.DIRECTION to CallTable.Direction.serialize(direction),
CallTable.EVENT to CallTable.Event.serialize(event),
CallTable.TIMESTAMP to timestamp
)
SignalDatabase.rawDatabase.insert(CallTable.TABLE_NAME, null, values)
SignalDatabase.threads.update(threadId, true)
}
}
@Test
fun accountData() {
val context = ApplicationDependencies.getApplication()
backupTest(validateKeyValue = true) {
val self = Recipient.self()
// TODO note-to-self archived
// TODO note-to-self unread
SignalStore.account().setAci(SELF_ACI)
SignalStore.account().setPni(SELF_PNI)
SignalStore.account().setE164(SELF_E164)
SignalStore.account().generateAciIdentityKeyIfNecessary()
SignalStore.account().generatePniIdentityKeyIfNecessary()
SignalDatabase.recipients.setProfileKey(self.id, ProfileKey(Random.nextBytes(32)))
SignalDatabase.recipients.setProfileName(self.id, ProfileName.fromParts("Peter", "Parker"))
SignalDatabase.recipients.setProfileAvatar(self.id, "https://example.com/")
SignalStore.donationsValues().markUserManuallyCancelled()
SignalStore.donationsValues().setSubscriber(Subscriber(SubscriberId.generate(), "USD"))
SignalStore.donationsValues().setDisplayBadgesOnProfile(false)
SignalStore.phoneNumberPrivacy().phoneNumberDiscoverabilityMode = PhoneNumberPrivacyValues.PhoneNumberDiscoverabilityMode.NOT_DISCOVERABLE
SignalStore.phoneNumberPrivacy().phoneNumberSharingMode = PhoneNumberPrivacyValues.PhoneNumberSharingMode.NOBODY
SignalStore.settings().isLinkPreviewsEnabled = false
SignalStore.settings().isPreferSystemContactPhotos = true
SignalStore.settings().universalExpireTimer = 42
SignalStore.settings().setKeepMutedChatsArchived(true)
SignalStore.storyValues().viewedReceiptsEnabled = false
SignalStore.storyValues().userHasViewedOnboardingStory = true
SignalStore.storyValues().isFeatureDisabled = false
SignalStore.storyValues().userHasBeenNotifiedAboutStories = true
SignalStore.storyValues().userHasSeenGroupStoryEducationSheet = true
SignalStore.emojiValues().reactions = listOf("a", "b", "c")
TextSecurePreferences.setTypingIndicatorsEnabled(context, false)
TextSecurePreferences.setReadReceiptsEnabled(context, false)
TextSecurePreferences.setShowUnidentifiedDeliveryIndicatorsEnabled(context, true)
}
// Have to check TextSecurePreferences ourselves, since they're not in a database
TextSecurePreferences.isTypingIndicatorsEnabled(context) assertIs false
TextSecurePreferences.isReadReceiptsEnabled(context) assertIs false
TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(context) assertIs true
}
/**
* Sets up the database, then executes your setup code, then compares snapshots of the database
* before an after an import to ensure that no data was lost/changed.
*
* @param validateKeyValue If true, this will also validate the KeyValueDatabase. You only want to do this if you
* intend on setting most of the values. Otherwise stuff tends to not match since values are lazily written.
*/
private fun backupTest(validateKeyValue: Boolean = false, content: () -> Unit) {
// Under normal circumstances, My Story ends up being the first recipient in the table, and is added automatically.
// This screws with the tests by offsetting all the recipientIds in the initial state.
// Easiest way to get around this is to make the DB a true clean slate by clearing everything.
// (We only really need to clear Recipient/dlists, but doing everything to be consistent.)
SignalDatabase.distributionLists.clearAllDataForBackupRestore()
SignalDatabase.recipients.clearAllDataForBackupRestore()
SignalDatabase.messages.clearAllDataForBackupRestore()
SignalDatabase.threads.clearAllDataForBackupRestore()
// Again, for comparison purposes, because we always import self first, we want to ensure it's the first item
// in the table when we export.
individualRecipient(
aci = SELF_ACI,
pni = SELF_PNI,
e164 = SELF_E164,
profileKey = SELF_PROFILE_KEY,
profileSharing = true
)
content()
val startingMainData: DatabaseData = SignalDatabase.rawDatabase.readAllContents()
val startingKeyValueData: DatabaseData = if (validateKeyValue) SignalDatabase.rawDatabase.readAllContents() else emptyMap()
val exported: ByteArray = BackupRepository.export()
BackupRepository.import(length = exported.size.toLong(), inputStreamFactory = { ByteArrayInputStream(exported) }, selfData = BackupRepository.SelfData(SELF_ACI, SELF_PNI, SELF_E164, SELF_PROFILE_KEY))
val endingData: DatabaseData = SignalDatabase.rawDatabase.readAllContents()
val endingKeyValueData: DatabaseData = if (validateKeyValue) SignalDatabase.rawDatabase.readAllContents() else emptyMap()
assertDatabaseMatches(startingMainData, endingData)
assertDatabaseMatches(startingKeyValueData, endingKeyValueData)
}
private fun individualChat(aci: ACI, givenName: String, familyName: String? = null, init: IndividualChatCreator.() -> Unit) {
val recipientId = individualRecipient(aci = aci, givenName = givenName, familyName = familyName, profileSharing = true)
val threadId: Long = SignalDatabase.threads.getOrCreateThreadIdFor(recipientId, false)
IndividualChatCreator(SignalDatabase.rawDatabase, recipientId, threadId).init()
SignalDatabase.threads.update(threadId, false)
}
private fun individualRecipient(
aci: ACI? = null,
pni: PNI? = null,
e164: String? = null,
givenName: String? = null,
familyName: String? = null,
username: String? = null,
hidden: Boolean = false,
registeredState: RecipientTable.RegisteredState = RecipientTable.RegisteredState.UNKNOWN,
profileKey: ProfileKey? = null,
profileSharing: Boolean = false,
hideStory: Boolean = false
): RecipientId {
check(aci != null || pni != null || e164 != null)
val recipientId = SignalDatabase.recipients.getAndPossiblyMerge(aci, pni, e164, pniVerified = true, changeSelf = true)
if (givenName != null || familyName != null) {
SignalDatabase.recipients.setProfileName(recipientId, ProfileName.fromParts(givenName, familyName))
}
if (username != null) {
SignalDatabase.recipients.setUsername(recipientId, username)
}
if (registeredState == RecipientTable.RegisteredState.REGISTERED) {
SignalDatabase.recipients.markRegistered(recipientId, aci ?: pni!!)
} else if (registeredState == RecipientTable.RegisteredState.NOT_REGISTERED) {
SignalDatabase.recipients.markUnregistered(recipientId)
}
if (profileKey != null) {
SignalDatabase.recipients.setProfileKey(recipientId, profileKey)
}
SignalDatabase.recipients.setProfileSharing(recipientId, profileSharing)
SignalDatabase.recipients.setHideStory(recipientId, hideStory)
if (hidden) {
SignalDatabase.recipients.markHidden(recipientId)
}
return recipientId
}
private inner class IndividualChatCreator(
private val db: SQLiteDatabase,
private val recipientId: RecipientId,
private val threadId: Long
) {
fun standardMessage(
outgoing: Boolean,
sentTimestamp: Long = System.currentTimeMillis(),
receivedTimestamp: Long = if (outgoing) sentTimestamp else sentTimestamp + 1,
serverTimestamp: Long = sentTimestamp,
body: String? = null,
read: Boolean = true,
quotes: Long? = null,
quoteTargetMissing: Boolean = false,
randomMention: Boolean = false,
randomStyling: Boolean = false
): Long {
return db.insertMessage(
from = if (outgoing) Recipient.self().id else recipientId,
to = if (outgoing) recipientId else Recipient.self().id,
outgoing = outgoing,
threadId = threadId,
sentTimestamp = sentTimestamp,
receivedTimestamp = receivedTimestamp,
serverTimestamp = serverTimestamp,
body = body,
read = read,
quotes = quotes,
quoteTargetMissing = quoteTargetMissing,
randomMention = randomMention,
randomStyling = randomStyling
)
}
fun remoteDeletedMessage(
outgoing: Boolean,
sentTimestamp: Long = System.currentTimeMillis(),
receivedTimestamp: Long = if (outgoing) sentTimestamp else sentTimestamp + 1,
serverTimestamp: Long = sentTimestamp
): Long {
return db.insertMessage(
from = if (outgoing) Recipient.self().id else recipientId,
to = if (outgoing) recipientId else Recipient.self().id,
outgoing = outgoing,
threadId = threadId,
sentTimestamp = sentTimestamp,
receivedTimestamp = receivedTimestamp,
serverTimestamp = serverTimestamp,
remoteDeleted = true
)
}
}
private fun SQLiteDatabase.insertMessage(
from: RecipientId,
to: RecipientId,
outgoing: Boolean,
threadId: Long,
sentTimestamp: Long = System.currentTimeMillis(),
receivedTimestamp: Long = if (outgoing) sentTimestamp else sentTimestamp + 1,
serverTimestamp: Long = sentTimestamp,
body: String? = null,
read: Boolean = true,
quotes: Long? = null,
quoteTargetMissing: Boolean = false,
randomMention: Boolean = false,
randomStyling: Boolean = false,
remoteDeleted: Boolean = false
): Long {
val type = if (outgoing) {
MessageTypes.BASE_SENT_TYPE
} else {
MessageTypes.BASE_INBOX_TYPE
} or MessageTypes.SECURE_MESSAGE_BIT or MessageTypes.PUSH_MESSAGE_BIT
val contentValues = ContentValues()
contentValues.put(MessageTable.DATE_SENT, sentTimestamp)
contentValues.put(MessageTable.DATE_RECEIVED, receivedTimestamp)
contentValues.put(MessageTable.FROM_RECIPIENT_ID, from.serialize())
contentValues.put(MessageTable.TO_RECIPIENT_ID, to.serialize())
contentValues.put(MessageTable.THREAD_ID, threadId)
contentValues.put(MessageTable.BODY, body)
contentValues.put(MessageTable.TYPE, type)
contentValues.put(MessageTable.READ, if (read) 1 else 0)
if (!outgoing) {
contentValues.put(MessageTable.DATE_SERVER, serverTimestamp)
}
if (remoteDeleted) {
contentValues.put(MessageTable.REMOTE_DELETED, 1)
return this
.insertInto(MessageTable.TABLE_NAME)
.values(contentValues)
.run()
}
if (quotes != null) {
val quoteDetails = this.getQuoteDetailsFor(quotes)
contentValues.put(MessageTable.QUOTE_ID, if (quoteTargetMissing) MessageTable.QUOTE_TARGET_MISSING_ID else quoteDetails.quotedSentTimestamp)
contentValues.put(MessageTable.QUOTE_AUTHOR, quoteDetails.authorId.serialize())
contentValues.put(MessageTable.QUOTE_BODY, quoteDetails.body)
contentValues.put(MessageTable.QUOTE_BODY_RANGES, quoteDetails.bodyRanges)
contentValues.put(MessageTable.QUOTE_TYPE, quoteDetails.type)
contentValues.put(MessageTable.QUOTE_MISSING, quoteTargetMissing.toInt())
}
if (body != null && (randomMention || randomStyling)) {
val ranges: MutableList<BodyRangeList.BodyRange> = mutableListOf()
if (randomMention) {
ranges += BodyRangeList.BodyRange(
start = 0,
length = Random.nextInt(body.length),
mentionUuid = if (outgoing) Recipient.resolved(to).requireAci().toString() else Recipient.resolved(from).requireAci().toString()
)
}
if (randomStyling) {
ranges += BodyRangeList.BodyRange(
start = 0,
length = Random.nextInt(body.length),
style = BodyRangeList.BodyRange.Style.fromValue(Random.nextInt(BodyRangeList.BodyRange.Style.values().size))
)
}
contentValues.put(MessageTable.MESSAGE_RANGES, BodyRangeList(ranges = ranges).encode())
}
return this
.insertInto(MessageTable.TABLE_NAME)
.values(contentValues)
.run()
}
private fun assertDatabaseMatches(expected: DatabaseData, actual: DatabaseData) {
assert(expected.keys.size == actual.keys.size) { "Mismatched table count! Expected: ${expected.keys} || Actual: ${actual.keys}" }
assert(expected.keys.containsAll(actual.keys)) { "Table names differ! Expected: ${expected.keys} || Actual: ${actual.keys}" }
val tablesToCheck = expected.keys.filter { !IGNORED_TABLES.contains(it) }
for (table in tablesToCheck) {
val expectedTable: List<Map<String, Any?>> = expected[table]!!
val actualTable: List<Map<String, Any?>> = actual[table]!!
assert(expectedTable.size == actualTable.size) { "Mismatched number of rows for table '$table'! Expected: ${expectedTable.size} || Actual: ${actualTable.size}\n $actualTable" }
val expectedFiltered: List<Map<String, Any?>> = expectedTable.withoutExcludedColumns(IGNORED_COLUMNS[table])
val actualFiltered: List<Map<String, Any?>> = actualTable.withoutExcludedColumns(IGNORED_COLUMNS[table])
assert(contentEquals(expectedFiltered, actualFiltered)) { "Data did not match for table '$table'!\n${prettyDiff(expectedFiltered, actualFiltered)}" }
}
}
private fun contentEquals(expectedRows: List<Map<String, Any?>>, actualRows: List<Map<String, Any?>>): Boolean {
if (expectedRows == actualRows) {
return true
}
assert(expectedRows.size == actualRows.size)
for (i in expectedRows.indices) {
val expectedRow = expectedRows[i]
val actualRow = actualRows[i]
for (key in expectedRow.keys) {
val expectedValue = expectedRow[key]
val actualValue = actualRow[key]
if (!contentEquals(expectedValue, actualValue)) {
return false
}
}
}
return true
}
private fun contentEquals(lhs: Any?, rhs: Any?): Boolean {
return if (lhs is ByteArray && rhs is ByteArray) {
lhs.contentEquals(rhs)
} else {
lhs == rhs
}
}
private fun prettyDiff(expectedRows: List<Map<String, Any?>>, actualRows: List<Map<String, Any?>>): String {
val builder = StringBuilder()
assert(expectedRows.size == actualRows.size)
for (i in expectedRows.indices) {
val expectedRow = expectedRows[i]
val actualRow = actualRows[i]
var describedRow = false
for (key in expectedRow.keys) {
val expectedValue = expectedRow[key]
val actualValue = actualRow[key]
if (!contentEquals(expectedValue, actualValue)) {
if (!describedRow) {
builder.append("-- ROW ${i + 1}\n")
describedRow = true
}
builder.append("  [$key] Expected: ${expectedValue.prettyPrint()} || Actual: ${actualValue.prettyPrint()} \n")
}
}
if (describedRow) {
builder.append("\n")
builder.append("Expected: $expectedRow\n")
builder.append("Actual: $actualRow\n")
}
}
return builder.toString()
}
private fun Any?.prettyPrint(): String {
return when (this) {
is ByteArray -> "Bytes(${Hex.toString(this)})"
else -> this.toString()
}
}
private fun List<Map<String, Any?>>.withoutExcludedColumns(ignored: Set<String>?): List<Map<String, Any?>> {
return if (ignored != null) {
this.map { row ->
row.filterKeys { !ignored.contains(it) }
}
} else {
this
}
}
private fun SQLiteDatabase.getQuoteDetailsFor(messageId: Long): QuoteDetails {
return this
.select(
MessageTable.DATE_SENT,
MessageTable.FROM_RECIPIENT_ID,
MessageTable.BODY,
MessageTable.MESSAGE_RANGES
)
.from(MessageTable.TABLE_NAME)
.where("${MessageTable.ID} = ?", messageId)
.run()
.readToSingleObject { cursor ->
QuoteDetails(
quotedSentTimestamp = cursor.requireLong(MessageTable.DATE_SENT),
authorId = RecipientId.from(cursor.requireLong(MessageTable.FROM_RECIPIENT_ID)),
body = cursor.requireString(MessageTable.BODY),
bodyRanges = cursor.requireBlob(MessageTable.MESSAGE_RANGES),
type = QuoteModel.Type.NORMAL.code
)
}!!
}
private fun SQLiteDatabase.readAllContents(): DatabaseData {
return SqlUtil.getAllTables(this).associateWith { table -> this.getAllTableData(table) }
}
private fun SQLiteDatabase.getAllTableData(table: String): List<Map<String, Any?>> {
return this
.select()
.from(table)
.run()
.readToList { cursor ->
val map: MutableMap<String, Any?> = mutableMapOf()
for (i in 0 until cursor.columnCount) {
val column = cursor.getColumnName(i)
when (cursor.getType(i)) {
Cursor.FIELD_TYPE_INTEGER -> map[column] = cursor.getInt(i)
Cursor.FIELD_TYPE_FLOAT -> map[column] = cursor.getFloat(i)
Cursor.FIELD_TYPE_STRING -> map[column] = cursor.getString(i)
Cursor.FIELD_TYPE_BLOB -> map[column] = cursor.getBlob(i)
Cursor.FIELD_TYPE_NULL -> map[column] = null
}
}
map
}
}
private data class QuoteDetails(
val quotedSentTimestamp: Long,
val authorId: RecipientId,
val body: String?,
val bodyRanges: ByteArray?,
val type: Int
)
}

View File

@@ -1,32 +0,0 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.thoughtcrime.securesms.testing.SignalFlakyTest
import org.thoughtcrime.securesms.testing.SignalFlakyTestRule
@RunWith(AndroidJUnit4::class)
class FlakyTestAnnotationTest {
@get:Rule
val flakyTestRule = SignalFlakyTestRule()
companion object {
private var count = 0
}
@SignalFlakyTest
@Test
fun purposelyFlaky() {
count++
assertEquals(3, count)
}
}

View File

@@ -1,48 +0,0 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil
import org.whispersystems.signalservice.api.util.toByteArray
import java.util.UUID
import kotlin.random.Random
object TestRecipientUtils {
private var upperGenAci = 13131313L
private var lowerGenAci = 0L
private var upperGenPni = 12121212L
private var lowerGenPni = 0L
private var groupMasterKeyRandom = Random(12345)
fun generateProfileKey(): ByteArray {
return ProfileKeyUtil.createNew().serialize()
}
fun nextPni(): ByteArray {
synchronized(this) {
lowerGenPni++
var uuid = UUID(upperGenPni, lowerGenPni)
return uuid.toByteArray()
}
}
fun nextAci(): ByteArray {
synchronized(this) {
lowerGenAci++
var uuid = UUID(upperGenAci, lowerGenAci)
return uuid.toByteArray()
}
}
fun generateGroupMasterKey(): ByteArray {
val masterKey = ByteArray(32)
groupMasterKeyRandom.nextBytes(masterKey)
return masterKey
}
}

View File

@@ -0,0 +1,393 @@
package org.thoughtcrime.securesms.components.settings.app.changenumber
import androidx.lifecycle.SavedStateHandle
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.FlakyTest
import okhttp3.mockwebserver.MockResponse
import org.junit.After
import org.junit.Before
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.signal.core.util.ThreadUtil
import org.signal.libsignal.protocol.state.SignedPreKeyRecord
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.dependencies.InstrumentationApplicationDependencyProvider
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.registration.VerifyAccountRepository
import org.thoughtcrime.securesms.registration.VerifyResponseProcessor
import org.thoughtcrime.securesms.testing.Get
import org.thoughtcrime.securesms.testing.MockProvider
import org.thoughtcrime.securesms.testing.Post
import org.thoughtcrime.securesms.testing.Put
import org.thoughtcrime.securesms.testing.SignalActivityRule
import org.thoughtcrime.securesms.testing.assertIs
import org.thoughtcrime.securesms.testing.assertIsNot
import org.thoughtcrime.securesms.testing.assertIsNotNull
import org.thoughtcrime.securesms.testing.assertIsNull
import org.thoughtcrime.securesms.testing.assertIsSize
import org.thoughtcrime.securesms.testing.connectionFailure
import org.thoughtcrime.securesms.testing.failure
import org.thoughtcrime.securesms.testing.parsedRequestBody
import org.thoughtcrime.securesms.testing.success
import org.thoughtcrime.securesms.testing.timeout
import org.whispersystems.signalservice.api.account.ChangePhoneNumberRequest
import org.whispersystems.signalservice.api.push.ServiceId
import org.whispersystems.signalservice.api.push.ServiceId.PNI
import org.whispersystems.signalservice.internal.push.MismatchedDevices
import org.whispersystems.signalservice.internal.push.PreKeyState
import java.util.UUID
@RunWith(AndroidJUnit4::class)
class ChangeNumberViewModelTest {
@get:Rule
val harness = SignalActivityRule()
private lateinit var viewModel: ChangeNumberViewModel
@Before
fun setUp() {
ThreadUtil.runOnMainSync {
viewModel = ChangeNumberViewModel(
localNumber = harness.self.requireE164(),
changeNumberRepository = ChangeNumberRepository(),
savedState = SavedStateHandle(),
password = SignalStore.account().servicePassword!!,
verifyAccountRepository = VerifyAccountRepository(harness.application)
)
viewModel.setNewCountry(1)
viewModel.setNewNationalNumber("5555550102")
}
}
@After
fun tearDown() {
InstrumentationApplicationDependencyProvider.clearHandlers()
}
@Test
fun testChangeNumber_givenOnlyPrimaryAndNoRegLock() {
// GIVEN
val aci = Recipient.self().requireServiceId()
val newPni = PNI.from(UUID.randomUUID())
lateinit var changeNumberRequest: ChangePhoneNumberRequest
lateinit var setPreKeysRequest: PreKeyState
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
Post("/v1/verification/session") { MockResponse().success(MockProvider.sessionMetadataJson.copy(verified = false)) },
Put("/v1/verification/session/${MockProvider.sessionMetadataJson.id}/code") { MockResponse().success(MockProvider.sessionMetadataJson) },
Get("/v1/devices") { MockResponse().success(MockProvider.primaryOnlyDeviceList) },
Put("/v2/accounts/number") { r ->
changeNumberRequest = r.parsedRequestBody()
MockResponse().success(MockProvider.createVerifyAccountResponse(aci, newPni))
},
Put("/v2/keys") { r ->
setPreKeysRequest = r.parsedRequestBody()
MockResponse().success()
},
Get("/v1/certificate/delivery") { MockResponse().success(MockProvider.senderCertificate) }
)
// WHEN
viewModel.requestVerificationCode(VerifyAccountRepository.Mode.SMS_WITHOUT_LISTENER, null, null).blockingGet().resultOrThrow
viewModel.verifyCodeWithoutRegistrationLock("123456").blockingGet().resultOrThrow
// THEN
assertSuccess(newPni, changeNumberRequest, setPreKeysRequest)
}
/**
* If we encounter a server error, this means the server ack our request and rejected it. In this
* case we know the change *did not* take on the server and can reset to a clean state.
*/
@Test
fun testChangeNumber_givenServerFailedApiCall() {
// GIVEN
val oldPni = Recipient.self().requirePni()
val oldE164 = Recipient.self().requireE164()
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
Post("/v1/verification/session") { MockResponse().success(MockProvider.sessionMetadataJson.copy(verified = false)) },
Put("/v1/verification/session/${MockProvider.sessionMetadataJson.id}/code") { MockResponse().success(MockProvider.sessionMetadataJson) },
Get("/v1/devices") { MockResponse().success(MockProvider.primaryOnlyDeviceList) },
Put("/v2/accounts/number") { MockResponse().failure(500) }
)
// WHEN
viewModel.requestVerificationCode(VerifyAccountRepository.Mode.SMS_WITHOUT_LISTENER, null, null).blockingGet().resultOrThrow
val processor: VerifyResponseProcessor = viewModel.verifyCodeWithoutRegistrationLock("123456").blockingGet()
// THEN
processor.isServerSentError() assertIs true
Recipient.self().requireE164() assertIs oldE164
Recipient.self().requirePni() assertIs oldPni
SignalStore.misc().pendingChangeNumberMetadata.assertIsNull()
}
/**
* If we encounter a non-server error like a timeout or bad SSL, we do not know the state of our change
* number on the server side. We have to do a whoami call to query the server for our details and then
* respond accordingly.
*
* In this case, the whoami is our old details, so we can know the change *did not* take on the server
* and can reset to a clean state.
*/
@Test
fun testChangeNumber_givenNetworkFailedApiCallEnRouteToServer() {
// GIVEN
val aci = Recipient.self().requireServiceId()
val oldPni = Recipient.self().requirePni()
val oldE164 = Recipient.self().requireE164()
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
Post("/v1/verification/session") { MockResponse().success(MockProvider.sessionMetadataJson.copy(verified = false)) },
Put("/v1/verification/session/${MockProvider.sessionMetadataJson.id}/code") { MockResponse().success(MockProvider.sessionMetadataJson) },
Get("/v1/devices") { MockResponse().success(MockProvider.primaryOnlyDeviceList) },
Put("/v2/accounts/number") { MockResponse().connectionFailure() },
Get("/v1/accounts/whoami") { MockResponse().success(MockProvider.createWhoAmIResponse(aci, oldPni, oldE164)) }
)
// WHEN
viewModel.requestVerificationCode(VerifyAccountRepository.Mode.SMS_WITHOUT_LISTENER, null, null).blockingGet().resultOrThrow
val processor: VerifyResponseProcessor = viewModel.verifyCodeWithoutRegistrationLock("123456").blockingGet()
// THEN
processor.isServerSentError() assertIs false
Recipient.self().requireE164() assertIs oldE164
Recipient.self().requirePni() assertIs oldPni
SignalStore.misc().isChangeNumberLocked assertIs false
SignalStore.misc().pendingChangeNumberMetadata.assertIsNull()
}
/**
* If we encounter a non-server error like a timeout or bad SSL, we do not know the state of our change
* number on the server side. We have to do a whoami call to query the server for our details and then
* respond accordingly.
*
* In this case, the whoami is our new details, so we can know the change *did* take on the server
* and need to keep the app in a locked state. The test then uses the ChangeNumberLockActivity to unlock
* and apply the pending state after confirming the change on the server.
*/
@Test
@FlakyTest
@Ignore("Test sometimes requires manual intervention to continue.")
fun testChangeNumber_givenNetworkFailedApiCallEnRouteToClient() {
// GIVEN
val aci = Recipient.self().requireServiceId()
val oldPni = Recipient.self().requirePni()
val oldE164 = Recipient.self().requireE164()
val newPni = PNI.from(UUID.randomUUID())
lateinit var changeNumberRequest: ChangePhoneNumberRequest
lateinit var setPreKeysRequest: PreKeyState
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
Post("/v1/verification/session") { MockResponse().success(MockProvider.sessionMetadataJson.copy(verified = false)) },
Put("/v1/verification/session/${MockProvider.sessionMetadataJson.id}/code") { MockResponse().success(MockProvider.sessionMetadataJson) },
Get("/v1/devices") { MockResponse().success(MockProvider.primaryOnlyDeviceList) },
Put("/v2/accounts/number") { r ->
changeNumberRequest = r.parsedRequestBody()
MockResponse().timeout()
},
Get("/v1/accounts/whoami") { MockResponse().success(MockProvider.createWhoAmIResponse(aci, newPni, "+15555550102")) },
Put("/v2/keys") { r ->
setPreKeysRequest = r.parsedRequestBody()
MockResponse().success()
},
Get("/v1/certificate/delivery") { MockResponse().success(MockProvider.senderCertificate) }
)
// WHEN
viewModel.requestVerificationCode(VerifyAccountRepository.Mode.SMS_WITHOUT_LISTENER, null, null).blockingGet().resultOrThrow
val processor: VerifyResponseProcessor = viewModel.verifyCodeWithoutRegistrationLock("123456").blockingGet()
// THEN
processor.isServerSentError() assertIs false
Recipient.self().requireE164() assertIs oldE164
Recipient.self().requirePni() assertIs oldPni
SignalStore.misc().isChangeNumberLocked assertIs true
SignalStore.misc().pendingChangeNumberMetadata.assertIsNotNull()
// WHEN AGAIN Processing lock
val scenario = harness.launchActivity<ChangeNumberLockActivity>()
scenario.onActivity {}
ThreadUtil.sleep(500)
// THEN AGAIN
assertSuccess(newPni, changeNumberRequest, setPreKeysRequest)
}
@Test
fun testChangeNumber_givenOnlyPrimaryAndRegistrationLock() {
// GIVEN
val aci = Recipient.self().requireServiceId()
val newPni = PNI.from(UUID.randomUUID())
lateinit var changeNumberRequest: ChangePhoneNumberRequest
lateinit var setPreKeysRequest: PreKeyState
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
Post("/v1/verification/session") { MockResponse().success(MockProvider.sessionMetadataJson.copy(verified = false)) },
Put("/v1/verification/session/${MockProvider.sessionMetadataJson.id}/code") { MockResponse().success(MockProvider.sessionMetadataJson) },
Get("/v1/devices") { MockResponse().success(MockProvider.primaryOnlyDeviceList) },
Put("/v2/accounts/number") { r ->
changeNumberRequest = r.parsedRequestBody()
if (changeNumberRequest.registrationLock.isNullOrEmpty()) {
MockResponse().failure(423, MockProvider.lockedFailure)
} else {
MockResponse().success(MockProvider.createVerifyAccountResponse(aci, newPni))
}
},
Put("/v2/keys") { r ->
setPreKeysRequest = r.parsedRequestBody()
MockResponse().success()
},
Get("/v1/certificate/delivery") { MockResponse().success(MockProvider.senderCertificate) }
)
// WHEN
viewModel.requestVerificationCode(VerifyAccountRepository.Mode.SMS_WITHOUT_LISTENER, null, null).blockingGet().resultOrThrow
viewModel.verifyCodeWithoutRegistrationLock("123456").blockingGet().also { processor ->
processor.registrationLock() assertIs true
Recipient.self().requirePni() assertIsNot newPni
SignalStore.misc().pendingChangeNumberMetadata.assertIsNull()
}
viewModel.verifyCodeAndRegisterAccountWithRegistrationLock("pin").blockingGet().resultOrThrow
// THEN
assertSuccess(newPni, changeNumberRequest, setPreKeysRequest)
}
@Test
fun testChangeNumber_givenMismatchedDevicesOnFirstCall() {
// GIVEN
val aci = Recipient.self().requireServiceId()
val newPni = PNI.from(UUID.randomUUID())
lateinit var changeNumberRequest: ChangePhoneNumberRequest
lateinit var setPreKeysRequest: PreKeyState
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
Post("/v1/verification/session") { MockResponse().success(MockProvider.sessionMetadataJson.copy(verified = false)) },
Put("/v1/verification/session/${MockProvider.sessionMetadataJson.id}/code") { MockResponse().success(MockProvider.sessionMetadataJson) },
Get("/v1/devices") { MockResponse().success(MockProvider.primaryOnlyDeviceList) },
Put("/v2/accounts/number") { r ->
changeNumberRequest = r.parsedRequestBody()
if (changeNumberRequest.deviceMessages.isEmpty()) {
MockResponse().failure(
409,
MismatchedDevices().apply {
missingDevices = listOf(2)
extraDevices = emptyList()
}
)
} else {
MockResponse().success(MockProvider.createVerifyAccountResponse(aci, newPni))
}
},
Get("/v2/keys/$aci/2") {
MockResponse().success(MockProvider.createPreKeyResponse(deviceId = 2))
},
Put("/v2/keys") { r ->
setPreKeysRequest = r.parsedRequestBody()
MockResponse().success()
},
Get("/v1/certificate/delivery") { MockResponse().success(MockProvider.senderCertificate) }
)
// WHEN
viewModel.requestVerificationCode(VerifyAccountRepository.Mode.SMS_WITHOUT_LISTENER, null, null).blockingGet().resultOrThrow
viewModel.verifyCodeWithoutRegistrationLock("123456").blockingGet().resultOrThrow
// THEN
assertSuccess(newPni, changeNumberRequest, setPreKeysRequest)
}
@Test
fun testChangeNumber_givenRegLockAndMismatchedDevicesOnFirstTwoCalls() {
// GIVEN
val aci = Recipient.self().requireServiceId()
val newPni = PNI.from(UUID.randomUUID())
lateinit var changeNumberRequest: ChangePhoneNumberRequest
lateinit var setPreKeysRequest: PreKeyState
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
Post("/v1/verification/session") { MockResponse().success(MockProvider.sessionMetadataJson.copy(verified = false)) },
Put("/v1/verification/session/${MockProvider.sessionMetadataJson.id}/code") { MockResponse().success(MockProvider.sessionMetadataJson) },
Put("/v2/accounts/number") { r ->
changeNumberRequest = r.parsedRequestBody()
if (changeNumberRequest.registrationLock.isNullOrEmpty()) {
MockResponse().failure(423, MockProvider.lockedFailure)
} else if (changeNumberRequest.deviceMessages.isEmpty()) {
MockResponse().failure(
409,
MismatchedDevices().apply {
missingDevices = listOf(2)
extraDevices = emptyList()
}
)
} else if (changeNumberRequest.deviceMessages.size == 1) {
MockResponse().failure(
409,
MismatchedDevices().apply {
missingDevices = listOf(2, 3)
extraDevices = emptyList()
}
)
} else {
MockResponse().success(MockProvider.createVerifyAccountResponse(aci, newPni))
}
},
Get("/v2/keys/$aci/2") {
MockResponse().success(MockProvider.createPreKeyResponse(deviceId = 2))
},
Get("/v2/keys/$aci/3") {
MockResponse().success(MockProvider.createPreKeyResponse(deviceId = 3))
},
Put("/v2/keys") { r ->
setPreKeysRequest = r.parsedRequestBody()
MockResponse().success()
},
Get("/v1/certificate/delivery") { MockResponse().success(MockProvider.senderCertificate) }
)
// WHEN
viewModel.requestVerificationCode(VerifyAccountRepository.Mode.SMS_WITHOUT_LISTENER, null, null).blockingGet().resultOrThrow
viewModel.verifyCodeWithoutRegistrationLock("123456").blockingGet().also { processor ->
processor.registrationLock() assertIs true
Recipient.self().requirePni() assertIsNot newPni
SignalStore.misc().pendingChangeNumberMetadata.assertIsNull()
}
viewModel.verifyCodeAndRegisterAccountWithRegistrationLock("pin").blockingGet().resultOrThrow
// THEN
assertSuccess(newPni, changeNumberRequest, setPreKeysRequest)
}
private fun assertSuccess(newPni: ServiceId, changeNumberRequest: ChangePhoneNumberRequest, setPreKeysRequest: PreKeyState) {
val pniProtocolStore = ApplicationDependencies.getProtocolStore().pni()
val pniMetadataStore = SignalStore.account().pniPreKeys
Recipient.self().requireE164() assertIs "+15555550102"
Recipient.self().requirePni() assertIs newPni
SignalStore.account().pniRegistrationId assertIs changeNumberRequest.pniRegistrationIds["1"]!!
SignalStore.account().pniIdentityKey.publicKey assertIs changeNumberRequest.pniIdentityKey
pniMetadataStore.activeSignedPreKeyId assertIs changeNumberRequest.devicePniSignedPrekeys["1"]!!.keyId
val activeSignedPreKey: SignedPreKeyRecord = pniProtocolStore.loadSignedPreKey(pniMetadataStore.activeSignedPreKeyId)
activeSignedPreKey.keyPair.publicKey assertIs changeNumberRequest.devicePniSignedPrekeys["1"]!!.publicKey
activeSignedPreKey.signature assertIs changeNumberRequest.devicePniSignedPrekeys["1"]!!.signature
setPreKeysRequest.signedPreKey.publicKey assertIs activeSignedPreKey.keyPair.publicKey
setPreKeysRequest.preKeys assertIsSize 100
SignalStore.misc().pendingChangeNumberMetadata.assertIsNull()
}
}

View File

@@ -1,133 +0,0 @@
package org.thoughtcrime.securesms.components.settings.app.subscription.donate
import androidx.test.core.app.ActivityScenario
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.isSelected
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import okhttp3.mockwebserver.MockResponse
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.signal.donations.InAppPaymentType
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
import org.thoughtcrime.securesms.dependencies.InstrumentationApplicationDependencyProvider
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.testing.Delete
import org.thoughtcrime.securesms.testing.Get
import org.thoughtcrime.securesms.testing.SignalActivityRule
import org.thoughtcrime.securesms.testing.success
import org.thoughtcrime.securesms.util.JsonUtils
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription
import org.whispersystems.signalservice.api.subscriptions.SubscriberId
import org.whispersystems.signalservice.internal.push.SubscriptionsConfiguration
import java.math.BigDecimal
import java.util.Currency
import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.milliseconds
@Ignore("Test fails on small screens, requires scrolling.")
@Suppress("ClassName")
@RunWith(AndroidJUnit4::class)
class CheckoutFlowActivityTest__RecurringDonations {
@get:Rule
val harness = SignalActivityRule(othersCount = 10)
private val intent = CheckoutFlowActivity.createIntent(InstrumentationRegistry.getInstrumentation().targetContext, InAppPaymentType.RECURRING_DONATION)
@Test
fun givenRecurringDonations_whenILoadScreen_thenIExpectMonthlySelected() {
ActivityScenario.launch<CheckoutFlowActivity>(intent)
onView(withId(R.id.monthly)).check(matches(isSelected()))
}
@Test
fun givenNoCurrentDonation_whenILoadScreen_thenIExpectContinueButton() {
ActivityScenario.launch<CheckoutFlowActivity>(intent)
onView(withText("Continue")).check(matches(isDisplayed()))
}
@Test
fun givenACurrentDonation_whenILoadScreen_thenIExpectUpgradeButton() {
initialiseConfigurationResponse()
initialiseActiveSubscription()
ActivityScenario.launch<CheckoutFlowActivity>(intent)
onView(withText(R.string.SubscribeFragment__update_subscription)).check(matches(isDisplayed()))
onView(withText(R.string.SubscribeFragment__cancel_subscription)).check(matches(isDisplayed()))
}
@Test
fun givenACurrentDonation_whenIPressCancel_thenIExpectCancellationDialog() {
initialiseConfigurationResponse()
initialiseActiveSubscription()
ActivityScenario.launch<CheckoutFlowActivity>(intent)
onView(withText(R.string.SubscribeFragment__cancel_subscription)).check(matches(isDisplayed()))
onView(withText(R.string.SubscribeFragment__cancel_subscription)).perform(ViewActions.click())
onView(withText(R.string.SubscribeFragment__confirm_cancellation)).check(matches(isDisplayed()))
onView(withText(R.string.SubscribeFragment__confirm)).perform(ViewActions.click())
onView(withText(R.string.StripePaymentInProgressFragment__cancelling)).check(matches(isDisplayed()))
}
private fun initialiseConfigurationResponse() {
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
Get("/v1/subscription/configuration") {
val assets = InstrumentationRegistry.getInstrumentation().context.resources.assets
assets.open("inAppPaymentsTests/configuration.json").use { stream ->
MockResponse().success(JsonUtils.fromJson(stream, SubscriptionsConfiguration::class.java))
}
}
)
}
private fun initialiseActiveSubscription() {
val currency = Currency.getInstance("USD")
val subscriber = InAppPaymentSubscriberRecord(
subscriberId = SubscriberId.generate(),
currency = currency,
type = InAppPaymentSubscriberRecord.Type.DONATION,
requiresCancel = false,
paymentMethodType = InAppPaymentData.PaymentMethodType.CARD
)
InAppPaymentsRepository.setSubscriber(subscriber)
SignalStore.inAppPayments.setSubscriberCurrency(currency, subscriber.type)
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
Get("/v1/subscription/${subscriber.subscriberId.serialize()}") {
MockResponse().success(
ActiveSubscription(
ActiveSubscription.Subscription(
200,
currency.currencyCode,
BigDecimal.ONE,
System.currentTimeMillis().milliseconds.inWholeSeconds + 30.days.inWholeSeconds,
true,
System.currentTimeMillis().milliseconds.inWholeSeconds + 30.days.inWholeSeconds,
false,
"active",
"STRIPE",
"CARD",
false
),
null
)
)
},
Delete("/v1/subscription/${subscriber.subscriberId.serialize()}") {
Thread.sleep(10000)
MockResponse().success()
}
)
}
}

View File

@@ -7,7 +7,6 @@ import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.signal.core.util.ThreadUtil
import org.thoughtcrime.securesms.attachments.Cdn
import org.thoughtcrime.securesms.attachments.PointerAttachment
import org.thoughtcrime.securesms.conversation.v2.ConversationActivity
import org.thoughtcrime.securesms.database.MessageType
@@ -16,6 +15,7 @@ import org.thoughtcrime.securesms.mms.IncomingMessage
import org.thoughtcrime.securesms.mms.OutgoingMessage
import org.thoughtcrime.securesms.profiles.ProfileName
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.releasechannel.ReleaseChannel
import org.thoughtcrime.securesms.testing.SignalActivityRule
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId
@@ -137,7 +137,7 @@ class ConversationItemPreviewer {
private fun attachment(): SignalServiceAttachmentPointer {
return SignalServiceAttachmentPointer(
Cdn.CDN_3.cdnNumber,
ReleaseChannel.CDN_NUMBER,
SignalServiceAttachmentRemoteId.from(""),
"image/webp",
null,
@@ -154,8 +154,7 @@ class ConversationItemPreviewer {
false,
Optional.empty(),
Optional.empty(),
System.currentTimeMillis(),
null
System.currentTimeMillis()
)
}
}

View File

@@ -12,7 +12,7 @@ import org.thoughtcrime.securesms.database.IdentityTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.DistributionListId
import org.thoughtcrime.securesms.database.model.DistributionListPrivacyMode
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.profiles.ProfileName
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.safety.SafetyNumberBottomSheet
@@ -64,7 +64,7 @@ class SafetyNumberChangeDialogPreviewer {
scenario.onActivity { conversationActivity ->
SafetyNumberBottomSheet
.forIdentityRecordsAndDestinations(
identityRecords = AppDependencies.protocolStore.aci().identities().getIdentityRecords(othersRecipients).identityRecords,
identityRecords = ApplicationDependencies.getProtocolStore().aci().identities().getIdentityRecords(othersRecipients).identityRecords,
destinations = listOf(ContactSearchKey.RecipientSearchKey(myStoryRecipientId, true))
)
.show(conversationActivity.supportFragmentManager)

View File

@@ -7,7 +7,6 @@ package org.thoughtcrime.securesms.conversation.v2.items
import android.net.Uri
import android.view.View
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.Observer
import com.bumptech.glide.RequestManager
import io.mockk.mockk
@@ -204,8 +203,8 @@ class V2ConversationItemShapeTest {
private val colorizer = Colorizer()
override val lifecycleOwner: LifecycleOwner = mockk(relaxed = true)
override val displayMode: ConversationItemDisplayMode = ConversationItemDisplayMode.Standard
override val clickListener: ConversationAdapter.ItemClickListener = FakeConversationItemClickListener
override val selectedItems: Set<MultiselectPart> = emptySet()
override val isMessageRequestAccepted: Boolean = true
@@ -290,8 +289,6 @@ class V2ConversationItemShapeTest {
override fun onChangeNumberUpdateContact(recipient: Recipient) = Unit
override fun onChangeProfileNameUpdateContact(recipient: Recipient) = Unit
override fun onCallToAction(action: String) = Unit
override fun onDonateClicked() = Unit
@@ -316,7 +313,7 @@ class V2ConversationItemShapeTest {
override fun goToMediaPreview(parent: ConversationItem?, sharedElement: View?, args: MediaIntentFactory.MediaPreviewArgs?) = Unit
override fun onEditedIndicatorClicked(conversationMessage: ConversationMessage) = Unit
override fun onEditedIndicatorClicked(messageRecord: MessageRecord) = Unit
override fun onShowGroupDescriptionClicked(groupName: String, description: String, shouldLinkifyWebLinks: Boolean) = Unit
@@ -331,8 +328,5 @@ class V2ConversationItemShapeTest {
override fun onReportSpamLearnMoreClicked() = Unit
override fun onMessageRequestAcceptOptionsClicked() = Unit
override fun onItemDoubleClick(item: MultiselectPart) = Unit
override fun onPaymentTombstoneClicked() = Unit
}
}

View File

@@ -51,16 +51,18 @@ class AttachmentTableTest {
SignalDatabase.attachments.updateAttachmentData(
attachment,
createMediaStream(byteArrayOf(1, 2, 3, 4, 5))
createMediaStream(byteArrayOf(1, 2, 3, 4, 5)),
false
)
SignalDatabase.attachments.updateAttachmentData(
attachment2,
createMediaStream(byteArrayOf(1, 2, 3))
createMediaStream(byteArrayOf(1, 2, 3)),
false
)
val attachment1Info = SignalDatabase.attachments.getDataFileInfo(attachment.attachmentId)
val attachment2Info = SignalDatabase.attachments.getDataFileInfo(attachment2.attachmentId)
val attachment1Info = SignalDatabase.attachments.getAttachmentDataFileInfo(attachment.attachmentId, AttachmentTable.DATA_FILE)
val attachment2Info = SignalDatabase.attachments.getAttachmentDataFileInfo(attachment2.attachmentId, AttachmentTable.DATA_FILE)
assertNotEquals(attachment1Info, attachment2Info)
}
@@ -77,16 +79,18 @@ class AttachmentTableTest {
SignalDatabase.attachments.updateAttachmentData(
attachment,
createMediaStream(byteArrayOf(1, 2, 3, 4, 5))
createMediaStream(byteArrayOf(1, 2, 3, 4, 5)),
true
)
SignalDatabase.attachments.updateAttachmentData(
attachment2,
createMediaStream(byteArrayOf(1, 2, 3, 4))
createMediaStream(byteArrayOf(1, 2, 3, 4)),
true
)
val attachment1Info = SignalDatabase.attachments.getDataFileInfo(attachment.attachmentId)
val attachment2Info = SignalDatabase.attachments.getDataFileInfo(attachment2.attachmentId)
val attachment1Info = SignalDatabase.attachments.getAttachmentDataFileInfo(attachment.attachmentId, AttachmentTable.DATA_FILE)
val attachment2Info = SignalDatabase.attachments.getAttachmentDataFileInfo(attachment2.attachmentId, AttachmentTable.DATA_FILE)
assertNotEquals(attachment1Info, attachment2Info)
}
@@ -117,14 +121,15 @@ class AttachmentTableTest {
val highDatabaseAttachment = SignalDatabase.attachments.insertAttachmentForPreUpload(highQualityPreUpload)
// WHEN
SignalDatabase.attachments.updateAttachmentData(standardDatabaseAttachment, createMediaStream(compressedData))
SignalDatabase.attachments.updateAttachmentData(standardDatabaseAttachment, createMediaStream(compressedData), false)
// THEN
val previousInfo = SignalDatabase.attachments.getDataFileInfo(previousDatabaseAttachmentId)!!
val standardInfo = SignalDatabase.attachments.getDataFileInfo(standardDatabaseAttachment.attachmentId)!!
val highInfo = SignalDatabase.attachments.getDataFileInfo(highDatabaseAttachment.attachmentId)!!
val previousInfo = SignalDatabase.attachments.getAttachmentDataFileInfo(previousDatabaseAttachmentId, AttachmentTable.DATA_FILE)!!
val standardInfo = SignalDatabase.attachments.getAttachmentDataFileInfo(standardDatabaseAttachment.attachmentId, AttachmentTable.DATA_FILE)!!
val highInfo = SignalDatabase.attachments.getAttachmentDataFileInfo(highDatabaseAttachment.attachmentId, AttachmentTable.DATA_FILE)!!
assertNotEquals(standardInfo, highInfo)
standardInfo.file assertIs previousInfo.file
highInfo.file assertIsNot standardInfo.file
highInfo.file.exists() assertIs true
}
@@ -153,9 +158,9 @@ class AttachmentTableTest {
val secondHighDatabaseAttachment = SignalDatabase.attachments.insertAttachmentForPreUpload(secondHighQualityPreUpload)
// THEN
val standardInfo = SignalDatabase.attachments.getDataFileInfo(standardDatabaseAttachment.attachmentId)!!
val highInfo = SignalDatabase.attachments.getDataFileInfo(highDatabaseAttachment.attachmentId)!!
val secondHighInfo = SignalDatabase.attachments.getDataFileInfo(secondHighDatabaseAttachment.attachmentId)!!
val standardInfo = SignalDatabase.attachments.getAttachmentDataFileInfo(standardDatabaseAttachment.attachmentId, AttachmentTable.DATA_FILE)!!
val highInfo = SignalDatabase.attachments.getAttachmentDataFileInfo(highDatabaseAttachment.attachmentId, AttachmentTable.DATA_FILE)!!
val secondHighInfo = SignalDatabase.attachments.getAttachmentDataFileInfo(secondHighDatabaseAttachment.attachmentId, AttachmentTable.DATA_FILE)!!
highInfo.file assertIsNot standardInfo.file
secondHighInfo.file assertIs highInfo.file

View File

@@ -1,838 +0,0 @@
package org.thoughtcrime.securesms.database
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Assert.assertArrayEquals
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.signal.core.util.Base64
import org.signal.core.util.update
import org.thoughtcrime.securesms.attachments.AttachmentId
import org.thoughtcrime.securesms.attachments.Cdn
import org.thoughtcrime.securesms.attachments.PointerAttachment
import org.thoughtcrime.securesms.backup.v2.BackupRepository.getMediaName
import org.thoughtcrime.securesms.database.AttachmentTable.TransformProperties
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.mms.MediaStream
import org.thoughtcrime.securesms.mms.OutgoingMessage
import org.thoughtcrime.securesms.mms.QuoteModel
import org.thoughtcrime.securesms.mms.SentMediaQuality
import org.thoughtcrime.securesms.providers.BlobProvider
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.MediaUtil
import org.thoughtcrime.securesms.util.Util
import org.whispersystems.signalservice.api.backup.MediaId
import org.whispersystems.signalservice.api.push.ServiceId
import java.io.File
import java.util.UUID
import kotlin.random.Random
import kotlin.time.Duration.Companion.days
/**
* Collection of [AttachmentTable] tests focused around deduping logic.
*/
@RunWith(AndroidJUnit4::class)
class AttachmentTableTest_deduping {
companion object {
val DATA_A = byteArrayOf(1, 2, 3)
val DATA_A_COMPRESSED = byteArrayOf(4, 5, 6)
val DATA_A_HASH = byteArrayOf(1, 1, 1)
val DATA_B = byteArrayOf(7, 8, 9)
}
@Before
fun setUp() {
SignalStore.account.setAci(ServiceId.ACI.from(UUID.randomUUID()))
SignalStore.account.setPni(ServiceId.PNI.from(UUID.randomUUID()))
SignalStore.account.setE164("+15558675309")
SignalDatabase.attachments.deleteAllAttachments()
}
/**
* Creates two different files with different data. Should not dedupe.
*/
@Test
fun differentFiles() {
test {
val id1 = insertWithData(DATA_A)
val id2 = insertWithData(DATA_B)
assertDataFilesAreDifferent(id1, id2)
}
}
/**
* Inserts files with identical data but with transform properties that make them incompatible. Should not dedupe.
*/
@Test
fun identicalFiles_incompatibleTransforms() {
// Non-matching qualities
test {
val id1 = insertWithData(DATA_A, TransformProperties(sentMediaQuality = SentMediaQuality.STANDARD.code))
val id2 = insertWithData(DATA_A, TransformProperties(sentMediaQuality = SentMediaQuality.HIGH.code))
assertDataFilesAreDifferent(id1, id2)
assertDataHashStartMatches(id1, id2)
}
// Non-matching video trim flag
test {
val id1 = insertWithData(DATA_A, TransformProperties())
val id2 = insertWithData(DATA_A, TransformProperties(videoTrim = true))
assertDataFilesAreDifferent(id1, id2)
assertDataHashStartMatches(id1, id2)
}
// Non-matching video trim start time
test {
val id1 = insertWithData(DATA_A, TransformProperties(videoTrim = true, videoTrimStartTimeUs = 1, videoTrimEndTimeUs = 2))
val id2 = insertWithData(DATA_A, TransformProperties(videoTrim = true, videoTrimStartTimeUs = 0, videoTrimEndTimeUs = 2))
assertDataFilesAreDifferent(id1, id2)
assertDataHashStartMatches(id1, id2)
}
// Non-matching video trim end time
test {
val id1 = insertWithData(DATA_A, TransformProperties(videoTrim = true, videoTrimStartTimeUs = 0, videoTrimEndTimeUs = 1))
val id2 = insertWithData(DATA_A, TransformProperties(videoTrim = true, videoTrimStartTimeUs = 0, videoTrimEndTimeUs = 2))
assertDataFilesAreDifferent(id1, id2)
assertDataHashStartMatches(id1, id2)
}
// Non-matching mp4 fast start
test {
val id1 = insertWithData(DATA_A, TransformProperties(mp4FastStart = true))
val id2 = insertWithData(DATA_A, TransformProperties(mp4FastStart = false))
assertDataFilesAreDifferent(id1, id2)
assertDataHashStartMatches(id1, id2)
}
}
/**
* Inserts files with identical data and compatible transform properties. Should dedupe.
*/
@Test
fun identicalFiles_compatibleTransforms() {
test {
val id1 = insertWithData(DATA_A)
val id2 = insertWithData(DATA_A)
assertDataFilesAreTheSame(id1, id2)
assertDataHashStartMatches(id1, id2)
assertSkipTransform(id1, false)
assertSkipTransform(id2, false)
}
test {
val id1 = insertWithData(DATA_A, TransformProperties(sentMediaQuality = SentMediaQuality.STANDARD.code))
val id2 = insertWithData(DATA_A, TransformProperties(sentMediaQuality = SentMediaQuality.STANDARD.code))
assertDataFilesAreTheSame(id1, id2)
assertDataHashStartMatches(id1, id2)
assertSkipTransform(id1, false)
assertSkipTransform(id2, false)
}
test {
val id1 = insertWithData(DATA_A, TransformProperties(sentMediaQuality = SentMediaQuality.HIGH.code))
val id2 = insertWithData(DATA_A, TransformProperties(sentMediaQuality = SentMediaQuality.HIGH.code))
assertDataFilesAreTheSame(id1, id2)
assertDataHashStartMatches(id1, id2)
assertSkipTransform(id1, false)
assertSkipTransform(id2, false)
}
test {
val id1 = insertWithData(DATA_A, TransformProperties(videoTrim = true, videoTrimStartTimeUs = 1, videoTrimEndTimeUs = 2))
val id2 = insertWithData(DATA_A, TransformProperties(videoTrim = true, videoTrimStartTimeUs = 1, videoTrimEndTimeUs = 2))
assertDataFilesAreTheSame(id1, id2)
assertDataHashStartMatches(id1, id2)
assertSkipTransform(id1, false)
assertSkipTransform(id2, false)
}
}
/**
* Walks through various scenarios where files are compressed and uploaded.
*/
@Test
fun compressionAndUploads() {
// Matches after the first is compressed, skip transform properly set
test {
val id1 = insertWithData(DATA_A)
compress(id1, DATA_A_COMPRESSED)
val id2 = insertWithData(DATA_A)
assertDataFilesAreTheSame(id1, id2)
assertDataHashStartMatches(id1, id2)
assertSkipTransform(id1, true)
assertSkipTransform(id2, true)
}
// Matches after the first is uploaded, skip transform and ending hash properly set
test {
val id1 = insertWithData(DATA_A)
compress(id1, DATA_A_COMPRESSED)
upload(id1)
val id2 = insertWithData(DATA_A)
assertDataFilesAreTheSame(id1, id2)
assertDataHashStartMatches(id1, id2)
assertDataHashEndMatches(id1, id2)
assertSkipTransform(id1, true)
assertSkipTransform(id2, true)
assertRemoteFieldsMatch(id1, id2)
assertArchiveFieldsMatch(id1, id2)
}
// Mimics sending two files at once. Ensures all fields are kept in sync as we compress and upload.
test {
val id1 = insertWithData(DATA_A)
val id2 = insertWithData(DATA_A)
assertDataFilesAreTheSame(id1, id2)
assertDataHashStartMatches(id1, id2)
assertSkipTransform(id1, false)
assertSkipTransform(id2, false)
compress(id1, DATA_A_COMPRESSED)
assertDataFilesAreTheSame(id1, id2)
assertDataHashStartMatches(id1, id2)
assertSkipTransform(id1, true)
assertSkipTransform(id2, true)
upload(id1)
assertDataFilesAreTheSame(id1, id2)
assertDataHashStartMatches(id1, id2)
assertDataHashEndMatches(id1, id2)
assertRemoteFieldsMatch(id1, id2)
assertArchiveFieldsMatch(id1, id2)
}
// Re-use the upload when uploaded recently
test {
val id1 = insertWithData(DATA_A)
compress(id1, DATA_A_COMPRESSED)
upload(id1, uploadTimestamp = System.currentTimeMillis())
val id2 = insertWithData(DATA_A)
assertDataFilesAreTheSame(id1, id2)
assertDataHashStartMatches(id1, id2)
assertDataHashEndMatches(id1, id2)
assertRemoteFieldsMatch(id1, id2)
assertArchiveFieldsMatch(id1, id2)
assertSkipTransform(id1, true)
assertSkipTransform(id2, true)
}
// Do not re-use old uploads
test {
val id1 = insertWithData(DATA_A)
compress(id1, DATA_A_COMPRESSED)
upload(id1, uploadTimestamp = System.currentTimeMillis() - 100.days.inWholeMilliseconds)
val id2 = insertWithData(DATA_A)
assertDataFilesAreTheSame(id1, id2)
assertDataHashStartMatches(id1, id2)
assertDataHashEndMatches(id1, id2)
assertSkipTransform(id1, true)
assertSkipTransform(id2, true)
assertDoesNotHaveRemoteFields(id2)
assertArchiveFieldsMatch(id1, id2)
}
// This isn't so much "desirable behavior" as it is documenting how things work.
// If an attachment is compressed but not uploaded yet, it will have a DATA_HASH_START that doesn't match the actual file content.
// This means that if we insert a new attachment with data that matches the compressed data, we won't find a match.
// This is ok because we don't allow forwarding unsent messages, so the chances of the user somehow sending a file that matches data we compressed are very low.
// What *is* more common is that the user may send DATA_A again, and in this case we will still catch the dedupe (which is already tested above).
test {
val id1 = insertWithData(DATA_A)
compress(id1, DATA_A_COMPRESSED)
val id2 = insertWithData(DATA_A_COMPRESSED)
assertDataFilesAreDifferent(id1, id2)
}
// This represents what would happen if you forward an already-send compressed attachment. We should match, skip transform, and skip upload.
test {
val id1 = insertWithData(DATA_A)
compress(id1, DATA_A_COMPRESSED)
upload(id1, uploadTimestamp = System.currentTimeMillis())
val id2 = insertWithData(DATA_A_COMPRESSED)
assertDataFilesAreTheSame(id1, id2)
assertDataHashEndMatches(id1, id2)
assertSkipTransform(id1, true)
assertSkipTransform(id1, true)
assertRemoteFieldsMatch(id1, id2)
assertArchiveFieldsMatch(id1, id2)
}
// This represents what would happen if you edited a video, sent it, then forwarded it. We should match, skip transform, and skip upload.
test {
val id1 = insertWithData(DATA_A, TransformProperties(videoTrim = true, videoTrimStartTimeUs = 1, videoTrimEndTimeUs = 2))
compress(id1, DATA_A_COMPRESSED)
upload(id1, uploadTimestamp = System.currentTimeMillis())
val id2 = insertWithData(DATA_A_COMPRESSED)
assertDataFilesAreTheSame(id1, id2)
assertDataHashEndMatches(id1, id2)
assertSkipTransform(id1, true)
assertSkipTransform(id1, true)
assertRemoteFieldsMatch(id1, id2)
assertArchiveFieldsMatch(id1, id2)
}
// This represents what would happen if you edited a video, sent it, then forwarded it, but *edited the forwarded video*. We should not dedupe.
test {
val id1 = insertWithData(DATA_A, TransformProperties(videoTrim = true, videoTrimStartTimeUs = 1, videoTrimEndTimeUs = 2))
compress(id1, DATA_A_COMPRESSED)
upload(id1, uploadTimestamp = System.currentTimeMillis())
val id2 = insertWithData(DATA_A_COMPRESSED, TransformProperties(videoTrim = true, videoTrimStartTimeUs = 1, videoTrimEndTimeUs = 2))
assertDataFilesAreDifferent(id1, id2)
assertSkipTransform(id1, true)
assertSkipTransform(id2, false)
assertDoesNotHaveRemoteFields(id2)
}
// This represents what would happen if you sent an image using standard quality, then forwarded it using high quality.
// Since you're forwarding, it doesn't matter if the new thing has a higher quality, we should still match and skip transform.
test {
val id1 = insertWithData(DATA_A, TransformProperties(sentMediaQuality = SentMediaQuality.STANDARD.code))
compress(id1, DATA_A_COMPRESSED)
upload(id1, uploadTimestamp = System.currentTimeMillis())
val id2 = insertWithData(DATA_A_COMPRESSED, TransformProperties(sentMediaQuality = SentMediaQuality.HIGH.code))
assertDataFilesAreTheSame(id1, id2)
assertDataHashEndMatches(id1, id2)
assertSkipTransform(id1, true)
assertSkipTransform(id1, true)
assertRemoteFieldsMatch(id1, id2)
assertArchiveFieldsMatch(id1, id2)
}
// This represents what would happen if you sent an image using high quality, then forwarded it using standard quality.
// Since you're forwarding, it doesn't matter if the new thing has a lower quality, we should still match and skip transform.
test {
val id1 = insertWithData(DATA_A, TransformProperties(sentMediaQuality = SentMediaQuality.HIGH.code))
compress(id1, DATA_A_COMPRESSED)
upload(id1, uploadTimestamp = System.currentTimeMillis())
val id2 = insertWithData(DATA_A_COMPRESSED, TransformProperties(sentMediaQuality = SentMediaQuality.STANDARD.code))
assertDataFilesAreTheSame(id1, id2)
assertDataHashEndMatches(id1, id2)
assertSkipTransform(id1, true)
assertSkipTransform(id1, true)
assertRemoteFieldsMatch(id1, id2)
assertArchiveFieldsMatch(id1, id2)
}
// Make sure that files marked as unhashable are all updated together
test {
val id1 = insertWithData(DATA_A)
val id2 = insertWithData(DATA_A)
upload(id1)
upload(id2)
clearHashes(id1)
clearHashes(id2)
val file = dataFile(id1)
SignalDatabase.attachments.markDataFileAsUnhashable(file)
assertDataFilesAreTheSame(id1, id2)
assertDataHashEndMatches(id1, id2)
val dataFileInfo = SignalDatabase.attachments.getDataFileInfo(id1)!!
assertTrue(dataFileInfo.hashEnd!!.startsWith("UNHASHABLE-"))
}
}
/**
* Various deletion scenarios to ensure that duped files don't deleted while there's still references.
*/
@Test
fun deletions() {
// Delete original then dupe
test {
val id1 = insertWithData(DATA_A)
val id2 = insertWithData(DATA_A)
val dataFile = dataFile(id1)
assertDataFilesAreTheSame(id1, id2)
delete(id1)
assertDeleted(id1)
assertRowAndFileExists(id2)
assertTrue(dataFile.exists())
delete(id2)
assertDeleted(id2)
assertFalse(dataFile.exists())
}
// Delete dupe then original
test {
val id1 = insertWithData(DATA_A)
val id2 = insertWithData(DATA_A)
val dataFile = dataFile(id1)
assertDataFilesAreTheSame(id1, id2)
delete(id2)
assertDeleted(id2)
assertRowAndFileExists(id1)
assertTrue(dataFile.exists())
delete(id1)
assertDeleted(id1)
assertFalse(dataFile.exists())
}
// Delete original after it was compressed
test {
val id1 = insertWithData(DATA_A)
compress(id1, DATA_A_COMPRESSED)
val id2 = insertWithData(DATA_A)
delete(id1)
assertDeleted(id1)
assertRowAndFileExists(id2)
assertSkipTransform(id2, true)
}
// Quotes are weak references and should not prevent us from deleting the file
test {
val id1 = insertWithData(DATA_A)
val id2 = insertQuote(id1)
val dataFile = dataFile(id1)
delete(id1)
assertDeleted(id1)
assertRowExists(id2)
assertFalse(dataFile.exists())
}
}
@Test
fun quotes() {
// Basic quote deduping
test {
val id1 = insertWithData(DATA_A)
val id2 = insertQuote(id1)
assertDataFilesAreTheSame(id1, id2)
assertDataHashStartMatches(id1, id2)
}
// Making sure remote fields carry
test {
val id1 = insertWithData(DATA_A)
val id2 = insertQuote(id1)
upload(id1)
assertDataFilesAreTheSame(id1, id2)
assertDataHashStartMatches(id1, id2)
assertDataHashEndMatches(id1, id2)
assertRemoteFieldsMatch(id1, id2)
assertArchiveFieldsMatch(id1, id2)
}
// Making sure things work for quotes of videos, which have trickier transform properties
test {
val id1 = insertWithData(DATA_A, transformProperties = TransformProperties.forVideoTrim(1, 2))
compress(id1, DATA_A_COMPRESSED)
upload(id1)
val id2 = insertQuote(id1)
assertDataFilesAreTheSame(id1, id2)
assertDataHashEndMatches(id1, id2)
assertRemoteFieldsMatch(id1, id2)
assertArchiveFieldsMatch(id1, id2)
}
}
/**
* Suite of tests around the migration where we hash all of the attachments and potentially dedupe them.
*/
@Test
fun migration() {
// Verifying that getUnhashedDataFile only returns if there's actually missing hashes
test {
val id = insertWithData(DATA_A)
upload(id)
assertNull(SignalDatabase.attachments.getUnhashedDataFile())
}
// Verifying that getUnhashedDataFile finds the missing hash
test {
val id = insertWithData(DATA_A)
upload(id)
clearHashes(id)
assertNotNull(SignalDatabase.attachments.getUnhashedDataFile())
}
// Verifying that getUnhashedDataFile doesn't return if the file isn't done downloading
test {
val id = insertWithData(DATA_A)
upload(id)
setTransferState(id, AttachmentTable.TRANSFER_PROGRESS_PENDING)
clearHashes(id)
assertNull(SignalDatabase.attachments.getUnhashedDataFile())
}
// If two attachments share the same file, when we backfill the hash, make sure both get their hashes set
test {
val id1 = insertWithData(DATA_A)
val id2 = insertWithData(DATA_A)
upload(id1)
upload(id2)
clearHashes(id1)
clearHashes(id2)
val file = dataFile(id1)
SignalDatabase.attachments.setHashForDataFile(file, DATA_A_HASH)
assertDataHashEnd(id1, DATA_A_HASH)
assertDataHashEndMatches(id1, id2)
}
// Creates a situation where two different attachments have the same data but wrote to different files, and verifies the migration dedupes it
test {
val id1 = insertWithData(DATA_A)
upload(id1)
clearHashes(id1)
val id2 = insertWithData(DATA_A)
upload(id2)
clearHashes(id2)
assertDataFilesAreDifferent(id1, id2)
val file1 = dataFile(id1)
SignalDatabase.attachments.setHashForDataFile(file1, DATA_A_HASH)
assertDataHashEnd(id1, DATA_A_HASH)
assertDataFilesAreDifferent(id1, id2)
val file2 = dataFile(id2)
SignalDatabase.attachments.setHashForDataFile(file2, DATA_A_HASH)
assertDataFilesAreTheSame(id1, id2)
assertDataHashEndMatches(id1, id2)
assertFalse(file2.exists())
}
// We've got three files now with the same data, with two of them sharing a file. We want to make sure *both* entries that share the same file get deduped.
test {
val id1 = insertWithData(DATA_A)
upload(id1)
clearHashes(id1)
val id2 = insertWithData(DATA_A)
val id3 = insertWithData(DATA_A)
upload(id2)
upload(id3)
clearHashes(id2)
clearHashes(id3)
assertDataFilesAreDifferent(id1, id2)
assertDataFilesAreTheSame(id2, id3)
val file1 = dataFile(id1)
SignalDatabase.attachments.setHashForDataFile(file1, DATA_A_HASH)
assertDataHashEnd(id1, DATA_A_HASH)
val file2 = dataFile(id2)
SignalDatabase.attachments.setHashForDataFile(file2, DATA_A_HASH)
assertDataFilesAreTheSame(id1, id2)
assertDataHashEndMatches(id1, id2)
assertDataHashEndMatches(id2, id3)
assertFalse(file2.exists())
}
// We don't want to mess with files that are still downloading, so this makes sure that even if data matches, we don't dedupe and don't delete the file
test {
val id1 = insertWithData(DATA_A)
upload(id1)
clearHashes(id1)
val id2 = insertWithData(DATA_A)
// *not* uploaded
clearHashes(id2)
assertDataFilesAreDifferent(id1, id2)
val file1 = dataFile(id1)
SignalDatabase.attachments.setHashForDataFile(file1, DATA_A_HASH)
assertDataHashEnd(id1, DATA_A_HASH)
val file2 = dataFile(id2)
SignalDatabase.attachments.setHashForDataFile(file2, DATA_A_HASH)
assertDataFilesAreDifferent(id1, id2)
assertTrue(file2.exists())
}
}
private class TestContext {
fun insertWithData(data: ByteArray, transformProperties: TransformProperties = TransformProperties.empty()): AttachmentId {
val uri = BlobProvider.getInstance().forData(data).createForSingleSessionInMemory()
val attachment = UriAttachmentBuilder.build(
id = Random.nextLong(),
uri = uri,
contentType = MediaUtil.IMAGE_JPEG,
transformProperties = transformProperties
)
return SignalDatabase.attachments.insertAttachmentForPreUpload(attachment).attachmentId
}
fun insertQuote(attachmentId: AttachmentId): AttachmentId {
val originalAttachment = SignalDatabase.attachments.getAttachment(attachmentId)!!
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.self())
val messageId = SignalDatabase.messages.insertMessageOutbox(
message = OutgoingMessage(
threadRecipient = Recipient.self(),
sentTimeMillis = System.currentTimeMillis(),
body = "some text",
outgoingQuote = QuoteModel(
id = 123,
author = Recipient.self().id,
text = "Some quote text",
isOriginalMissing = false,
attachments = listOf(originalAttachment),
mentions = emptyList(),
type = QuoteModel.Type.NORMAL,
bodyRanges = null
)
),
threadId = threadId,
forceSms = false,
insertListener = null
)
val attachments = SignalDatabase.attachments.getAttachmentsForMessage(messageId)
return attachments[0].attachmentId
}
fun compress(attachmentId: AttachmentId, newData: ByteArray, mp4FastStart: Boolean = false) {
val databaseAttachment = SignalDatabase.attachments.getAttachment(attachmentId)!!
SignalDatabase.attachments.updateAttachmentData(databaseAttachment, newData.asMediaStream())
SignalDatabase.attachments.markAttachmentAsTransformed(attachmentId, withFastStart = mp4FastStart)
}
fun upload(attachmentId: AttachmentId, uploadTimestamp: Long = System.currentTimeMillis()) {
SignalDatabase.attachments.finalizeAttachmentAfterUpload(attachmentId, createPointerAttachment(attachmentId, uploadTimestamp), uploadTimestamp)
val attachment = SignalDatabase.attachments.getAttachment(attachmentId)!!
SignalDatabase.attachments.setArchiveData(
attachmentId = attachmentId,
archiveCdn = Cdn.CDN_3.cdnNumber,
archiveMediaName = attachment.getMediaName().name,
archiveThumbnailMediaId = MediaId(Util.getSecretBytes(15)).encode(),
archiveMediaId = MediaId(Util.getSecretBytes(15)).encode()
)
}
fun delete(attachmentId: AttachmentId) {
SignalDatabase.attachments.deleteAttachment(attachmentId)
}
fun dataFile(attachmentId: AttachmentId): File {
return SignalDatabase.attachments.getDataFileInfo(attachmentId)!!.file
}
fun setTransferState(attachmentId: AttachmentId, transferState: Int) {
// messageId doesn't actually matter -- that's for notifying listeners
SignalDatabase.attachments.setTransferState(messageId = -1, attachmentId = attachmentId, transferState = transferState)
}
fun clearHashes(id: AttachmentId) {
SignalDatabase.attachments.writableDatabase
.update(AttachmentTable.TABLE_NAME)
.values(
AttachmentTable.DATA_HASH_START to null,
AttachmentTable.DATA_HASH_END to null
)
.where("${AttachmentTable.ID} = ?", id)
.run()
}
fun assertDeleted(attachmentId: AttachmentId) {
assertNull("$attachmentId exists, but it shouldn't!", SignalDatabase.attachments.getAttachment(attachmentId))
}
fun assertRowAndFileExists(attachmentId: AttachmentId) {
val databaseAttachment = SignalDatabase.attachments.getAttachment(attachmentId)
assertNotNull("$attachmentId does not exist!", databaseAttachment)
val dataFileInfo = SignalDatabase.attachments.getDataFileInfo(attachmentId)
assertTrue("The file for $attachmentId does not exist!", dataFileInfo!!.file.exists())
}
fun assertRowExists(attachmentId: AttachmentId) {
val databaseAttachment = SignalDatabase.attachments.getAttachment(attachmentId)
assertNotNull("$attachmentId does not exist!", databaseAttachment)
}
fun assertDataFilesAreTheSame(lhs: AttachmentId, rhs: AttachmentId) {
val lhsInfo = SignalDatabase.attachments.getDataFileInfo(lhs)!!
val rhsInfo = SignalDatabase.attachments.getDataFileInfo(rhs)!!
assert(lhsInfo.file.exists())
assert(rhsInfo.file.exists())
assertEquals(lhsInfo.file, rhsInfo.file)
assertEquals(lhsInfo.length, rhsInfo.length)
assertArrayEquals(lhsInfo.random, rhsInfo.random)
}
fun assertDataFilesAreDifferent(lhs: AttachmentId, rhs: AttachmentId) {
val lhsInfo = SignalDatabase.attachments.getDataFileInfo(lhs)!!
val rhsInfo = SignalDatabase.attachments.getDataFileInfo(rhs)!!
assert(lhsInfo.file.exists())
assert(rhsInfo.file.exists())
assertNotEquals(lhsInfo.file, rhsInfo.file)
}
fun assertDataHashStartMatches(lhs: AttachmentId, rhs: AttachmentId) {
val lhsInfo = SignalDatabase.attachments.getDataFileInfo(lhs)!!
val rhsInfo = SignalDatabase.attachments.getDataFileInfo(rhs)!!
assertNotNull(lhsInfo.hashStart)
assertEquals("DATA_HASH_START's did not match!", lhsInfo.hashStart, rhsInfo.hashStart)
}
fun assertDataHashEndMatches(lhs: AttachmentId, rhs: AttachmentId) {
val lhsInfo = SignalDatabase.attachments.getDataFileInfo(lhs)!!
val rhsInfo = SignalDatabase.attachments.getDataFileInfo(rhs)!!
assertNotNull(lhsInfo.hashEnd)
assertEquals("DATA_HASH_END's did not match!", lhsInfo.hashEnd, rhsInfo.hashEnd)
}
fun assertDataHashEnd(id: AttachmentId, byteArray: ByteArray) {
val dataFileInfo = SignalDatabase.attachments.getDataFileInfo(id)!!
assertArrayEquals(byteArray, Base64.decode(dataFileInfo.hashEnd!!))
}
fun assertRemoteFieldsMatch(lhs: AttachmentId, rhs: AttachmentId) {
val lhsAttachment = SignalDatabase.attachments.getAttachment(lhs)!!
val rhsAttachment = SignalDatabase.attachments.getAttachment(rhs)!!
assertEquals(lhsAttachment.remoteLocation, rhsAttachment.remoteLocation)
assertEquals(lhsAttachment.remoteKey, rhsAttachment.remoteKey)
assertArrayEquals(lhsAttachment.remoteDigest, rhsAttachment.remoteDigest)
assertArrayEquals(lhsAttachment.incrementalDigest, rhsAttachment.incrementalDigest)
assertEquals(lhsAttachment.incrementalMacChunkSize, rhsAttachment.incrementalMacChunkSize)
assertEquals(lhsAttachment.cdn.cdnNumber, rhsAttachment.cdn.cdnNumber)
}
fun assertArchiveFieldsMatch(lhs: AttachmentId, rhs: AttachmentId) {
val lhsAttachment = SignalDatabase.attachments.getAttachment(lhs)!!
val rhsAttachment = SignalDatabase.attachments.getAttachment(rhs)!!
assertEquals(lhsAttachment.archiveCdn, rhsAttachment.archiveCdn)
assertEquals(lhsAttachment.archiveMediaName, rhsAttachment.archiveMediaName)
assertEquals(lhsAttachment.archiveMediaId, rhsAttachment.archiveMediaId)
}
fun assertDoesNotHaveRemoteFields(attachmentId: AttachmentId) {
val databaseAttachment = SignalDatabase.attachments.getAttachment(attachmentId)!!
assertEquals(0, databaseAttachment.uploadTimestamp)
assertNull(databaseAttachment.remoteLocation)
assertNull(databaseAttachment.remoteDigest)
assertNull(databaseAttachment.remoteKey)
assertEquals(0, databaseAttachment.cdn.cdnNumber)
}
fun assertSkipTransform(attachmentId: AttachmentId, state: Boolean) {
val transformProperties = SignalDatabase.attachments.getTransformProperties(attachmentId)!!
assertEquals("Incorrect skipTransform!", transformProperties.skipTransform, state)
}
private fun ByteArray.asMediaStream(): MediaStream {
return MediaStream(this.inputStream(), MediaUtil.IMAGE_JPEG, 2, 2)
}
private fun createPointerAttachment(attachmentId: AttachmentId, uploadTimestamp: Long = System.currentTimeMillis()): PointerAttachment {
val location = "somewhere-${Random.nextLong()}"
val key = "somekey-${Random.nextLong()}"
val digest = Random.nextBytes(32)
val incrementalDigest = Random.nextBytes(16)
val databaseAttachment = SignalDatabase.attachments.getAttachment(attachmentId)!!
return PointerAttachment(
"image/jpeg",
AttachmentTable.TRANSFER_PROGRESS_DONE,
databaseAttachment.size, // size
null,
Cdn.CDN_3, // cdnNumber
location,
key,
digest,
incrementalDigest,
5, // incrementalMacChunkSize
null,
databaseAttachment.voiceNote,
databaseAttachment.borderless,
databaseAttachment.videoGif,
databaseAttachment.width,
databaseAttachment.height,
uploadTimestamp,
databaseAttachment.caption,
databaseAttachment.stickerLocator,
databaseAttachment.blurHash,
databaseAttachment.uuid
)
}
}
private fun test(content: TestContext.() -> Unit) {
SignalDatabase.attachments.deleteAllAttachments()
val context = TestContext()
context.content()
}
}

View File

@@ -56,7 +56,7 @@ class CallTableTest {
)
val call = SignalDatabase.calls.getCallById(callId, groupRecipientId)
SignalDatabase.calls.markCallDeletedFromSyncEvent(call!!)
SignalDatabase.calls.deleteGroupCall(call!!)
val deletedCall = SignalDatabase.calls.getCallById(callId, groupRecipientId)
val oldestDeletionTimestamp = SignalDatabase.calls.getOldestDeletionTimestamp()
@@ -69,10 +69,9 @@ class CallTableTest {
@Test
fun givenNoPreExistingEvent_whenIDeleteGroupCall_thenIInsertAndMarkCallDeleted() {
val callId = 1L
SignalDatabase.calls.insertDeletedCallFromSyncEvent(
SignalDatabase.calls.insertDeletedGroupCallFromSyncEvent(
callId,
groupRecipientId,
CallTable.Type.GROUP_CALL,
CallTable.Direction.OUTGOING,
System.currentTimeMillis()
)
@@ -439,12 +438,11 @@ class CallTableTest {
@Test
fun givenADeletedCallEvent_whenIReceiveARingUpdate_thenIIgnoreTheRingUpdate() {
val callId = 1L
SignalDatabase.calls.insertDeletedCallFromSyncEvent(
SignalDatabase.calls.insertDeletedGroupCallFromSyncEvent(
callId = callId,
recipientId = groupRecipientId,
direction = CallTable.Direction.INCOMING,
timestamp = System.currentTimeMillis(),
type = CallTable.Type.GROUP_CALL
timestamp = System.currentTimeMillis()
)
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(

View File

@@ -15,7 +15,7 @@ import org.signal.core.util.getIndexes
import org.signal.core.util.readToList
import org.signal.core.util.requireNonNullString
import org.thoughtcrime.securesms.database.helpers.SignalDatabaseMigrations
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.testing.SignalActivityRule
/**
@@ -30,7 +30,7 @@ class DatabaseConsistencyTest {
@Test
fun testUpgradeConsistency() {
val currentVersionStatements = SignalDatabase.rawDatabase.getAllCreateStatements()
val testHelper = InMemoryTestHelper(AppDependencies.application).also {
val testHelper = InMemoryTestHelper(ApplicationDependencies.getApplication()).also {
it.onUpgrade(it.writableDatabase, 181, SignalDatabaseMigrations.DATABASE_VERSION)
}

View File

@@ -8,7 +8,7 @@ import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.signal.core.util.concurrent.SignalExecutors
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import java.util.concurrent.CountDownLatch
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger
@@ -26,7 +26,7 @@ class DatabaseObserverTest {
@Before
fun setup() {
db = SignalDatabase.instance!!.signalWritableDatabase
observer = AppDependencies.databaseObserver
observer = ApplicationDependencies.getDatabaseObserver()
}
@Test

View File

@@ -25,6 +25,15 @@ class DistributionListTablesTest {
Assert.assertNotNull(id)
}
@Test
fun createList_whenNameConflict_failToInsert() {
val id: DistributionListId? = distributionDatabase.createList("test", recipientList(1, 2, 3))
Assert.assertNotNull(id)
val id2: DistributionListId? = distributionDatabase.createList("test", recipientList(1, 2, 3))
Assert.assertNull(id2)
}
@Test
fun getList_returnCorrectList() {
createRecipients(3)

View File

@@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.database
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotEquals
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Rule
@@ -74,6 +75,21 @@ class GroupTableTest {
assertEquals(2, groups.size)
}
@Test
fun givenGroups_whenIQueryGroupsByMembership_thenIExpectBothGroups() {
insertPushGroup()
insertMmsGroup(members = listOf(harness.others[1]))
val groups = groupTable.queryGroupsByMembership(
setOf(harness.self.id, harness.others[1]),
includeInactive = false,
excludeV1 = false,
excludeMms = false
)
assertEquals(2, groups.cursor?.count)
}
@Test
fun givenGroups_whenIGetGroups_thenIExpectBothGroups() {
insertPushGroup()
@@ -165,10 +181,72 @@ class GroupTableTest {
assertFalse(actual)
}
@Test
fun givenAGroup_whenIUpdateMembers_thenIExpectUpdatedMembers() {
val v2Group = insertPushGroup()
groupTable.updateMembers(v2Group, listOf(harness.self.id, harness.others[1]))
val groupRecord = groupTable.getGroup(v2Group)
assertEquals(setOf(harness.self.id, harness.others[1]), groupRecord.get().members.toSet())
}
@Test
fun givenAnMmsGroup_whenIGetOrCreateMmsGroup_thenIExpectMyMmsGroup() {
val members: List<RecipientId> = listOf(harness.self.id, harness.others[0])
val other = insertMmsGroup(members + listOf(harness.others[1]))
val mmsGroup = insertMmsGroup(members)
val actual = groupTable.getOrCreateMmsGroupForMembers(members.toSet())
assertNotEquals(other, actual)
assertEquals(mmsGroup, actual)
}
@Test
fun givenMultipleMmsGroups_whenIGetOrCreateMmsGroup_thenIExpectMyMmsGroup() {
val group1Members: List<RecipientId> = listOf(harness.self.id, harness.others[0], harness.others[1])
val group2Members: List<RecipientId> = listOf(harness.self.id, harness.others[0], harness.others[2])
val group1: GroupId = insertMmsGroup(group1Members)
val group2: GroupId = insertMmsGroup(group2Members)
val group1Result: GroupId = groupTable.getOrCreateMmsGroupForMembers(group1Members.toSet())
val group2Result: GroupId = groupTable.getOrCreateMmsGroupForMembers(group2Members.toSet())
assertEquals(group1, group1Result)
assertEquals(group2, group2Result)
assertNotEquals(group1Result, group2Result)
}
@Test
fun givenMultipleMmsGroupsWithDifferentMemberOrders_whenIGetOrCreateMmsGroup_thenIExpectMyMmsGroup() {
val group1Members: List<RecipientId> = listOf(harness.self.id, harness.others[0], harness.others[1], harness.others[2]).shuffled()
val group2Members: List<RecipientId> = listOf(harness.self.id, harness.others[0], harness.others[2], harness.others[3]).shuffled()
val group1: GroupId = insertMmsGroup(group1Members)
val group2: GroupId = insertMmsGroup(group2Members)
val group1Result: GroupId = groupTable.getOrCreateMmsGroupForMembers(group1Members.shuffled().toSet())
val group2Result: GroupId = groupTable.getOrCreateMmsGroupForMembers(group2Members.shuffled().toSet())
assertEquals(group1, group1Result)
assertEquals(group2, group2Result)
assertNotEquals(group1Result, group2Result)
}
@Test
fun givenMmsGroupWithOneMember_whenIGetOrCreateMmsGroup_thenIExpectMyMmsGroup() {
val groupMembers: List<RecipientId> = listOf(harness.self.id)
val group: GroupId = insertMmsGroup(groupMembers)
val groupResult: GroupId = groupTable.getOrCreateMmsGroupForMembers(groupMembers.toSet())
assertEquals(group, groupResult)
}
@Test
fun givenTwoGroupsWithoutMembers_whenIQueryThem_thenIExpectEach() {
val g1 = insertPushGroup(members = emptyList())
val g2 = insertPushGroup(members = emptyList())
val g1 = insertPushGroup(listOf())
val g2 = insertPushGroup(listOf())
val gr1 = groupTable.getGroup(g1)
val gr2 = groupTable.getGroup(g2)
@@ -195,85 +273,6 @@ class GroupTableTest {
assertEquals(groups[0].id, groupInCommon)
}
@Test
fun givenTwoGroupsWithANameThatSharesAToken_whenISearchForTheSharedToken_thenIExpectBothGroups() {
insertPushGroup("Group Alice")
insertPushGroup("Group Bob")
SignalDatabase.groups.queryGroupsByTitle(
inputQuery = "Group",
includeInactive = false,
excludeV1 = false,
excludeMms = false
).use {
assertEquals(2, it.cursor?.count)
val firstGroup = it.getNext()
val secondGroup = it.getNext()
assertEquals("Group Alice", firstGroup?.title)
assertEquals("Group Bob", secondGroup?.title)
}
}
@Test
fun givenTwoGroupsWithANameThatSharesAToken_whenISearchForAnUnsharedToken_thenIExpectOneGroup() {
insertPushGroup("Group Alice")
insertPushGroup("Group Bob")
SignalDatabase.groups.queryGroupsByTitle(
inputQuery = "Alice",
includeInactive = false,
excludeV1 = false,
excludeMms = false
).use {
assertEquals(1, it.cursor?.count)
val firstGroup = it.getNext()
assertEquals("Group Alice", firstGroup?.title)
}
}
@Test
fun givenAGroupWithThreeTokens_whenISearchForTheFirstAndLastToken_thenIExpectThatGroup() {
insertPushGroup("Group & Alice")
SignalDatabase.groups.queryGroupsByTitle(
inputQuery = "Group Alice",
includeInactive = false,
excludeV1 = false,
excludeMms = false
).use {
assertEquals(1, it.cursor?.count)
val firstGroup = it.getNext()
assertEquals("Group & Alice", firstGroup?.title)
}
}
@Test
fun givenTwoGroupsWithSharedTokens_whenISearchForAnExactMatch_thenIExpectThatGroupFirst() {
insertPushGroup("Group Alice Bob")
insertPushGroup("Group Bob")
SignalDatabase.groups.queryGroupsByTitle(
inputQuery = "Group Bob",
includeInactive = false,
excludeV1 = false,
excludeMms = false
).use {
assertEquals(2, it.cursor?.count)
val firstGroup = it.getNext()
val second = it.getNext()
assertEquals("Group Bob", firstGroup?.title)
assertEquals("Group Alice Bob", second?.title)
}
}
private fun insertThread(groupId: GroupId): Long {
val groupRecipient = SignalDatabase.recipients.getByGroupId(groupId).get()
return SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(groupRecipient))
@@ -293,7 +292,6 @@ class GroupTableTest {
}
private fun insertPushGroup(
title: String = "Test Group",
members: List<DecryptedMember> = listOf(
DecryptedMember.Builder()
.aciBytes(harness.self.requireAci().toByteString())
@@ -309,12 +307,11 @@ class GroupTableTest {
): GroupId {
val groupMasterKey = GroupMasterKey(Random.nextBytes(GroupMasterKey.SIZE))
val decryptedGroupState = DecryptedGroup.Builder()
.title(title)
.members(members)
.revision(0)
.build()
return groupTable.create(groupMasterKey, decryptedGroupState, null)!!
return groupTable.create(groupMasterKey, decryptedGroupState)!!
}
private fun insertPushGroupWithSelfAndOthers(others: List<RecipientId>): GroupId {
@@ -339,6 +336,6 @@ class GroupTableTest {
.revision(0)
.build()
return groupTable.create(groupMasterKey, decryptedGroupState, null)!!
return groupTable.create(groupMasterKey, decryptedGroupState)!!
}
}

View File

@@ -159,7 +159,7 @@ class KyberPreKeyTableTest {
val count = SignalDatabase.rawDatabase
.update(KyberPreKeyTable.TABLE_NAME)
.values(KyberPreKeyTable.STALE_TIMESTAMP to staleTime)
.where("${KyberPreKeyTable.ACCOUNT_ID} = ? AND ${KyberPreKeyTable.KEY_ID} = $id", account.toAccountId())
.where("${KyberPreKeyTable.ACCOUNT_ID} = ? AND ${KyberPreKeyTable.KEY_ID} = $id", account)
.run()
assertEquals(1, count)
@@ -169,15 +169,8 @@ class KyberPreKeyTableTest {
return SignalDatabase.rawDatabase
.select(KyberPreKeyTable.STALE_TIMESTAMP)
.from(KyberPreKeyTable.TABLE_NAME)
.where("${KyberPreKeyTable.ACCOUNT_ID} = ? AND ${KyberPreKeyTable.KEY_ID} = $id", account.toAccountId())
.where("${KyberPreKeyTable.ACCOUNT_ID} = ? AND ${KyberPreKeyTable.KEY_ID} = $id", account)
.run()
.readToSingleObject { it.requireLongOrNull(KyberPreKeyTable.STALE_TIMESTAMP) }
}
private fun ServiceId.toAccountId(): String {
return when (this) {
is ACI -> this.toString()
is PNI -> KyberPreKeyTable.PNI_ACCOUNT_ID
}
}
}

View File

@@ -12,12 +12,12 @@ import org.signal.core.util.requireNonNullString
import org.signal.core.util.select
import org.signal.core.util.updateAll
import org.thoughtcrime.securesms.crash.CrashConfig
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.testing.assertIs
class LogDatabaseTest {
private val db: LogDatabase = LogDatabase.getInstance(AppDependencies.application)
private val db: LogDatabase = LogDatabase.getInstance(ApplicationDependencies.getApplication())
@Test
fun crashTable_matchesNamePattern() {

View File

@@ -30,8 +30,8 @@ class MessageTableTest_gifts {
mms.deleteAllThreads()
SignalStore.account.setAci(localAci)
SignalStore.account.setPni(localPni)
SignalStore.account().setAci(localAci)
SignalStore.account().setPni(localPni)
recipients = (0 until 5).map { SignalDatabase.recipients.getOrInsertFromServiceId(ACI.from(UUID.randomUUID())) }
}

View File

@@ -40,14 +40,14 @@ class MmsTableTest_stories {
mms.deleteAllThreads()
SignalStore.account.setAci(localAci)
SignalStore.account.setPni(localPni)
SignalStore.account().setAci(localAci)
SignalStore.account().setPni(localPni)
myStory = Recipient.resolved(SignalDatabase.recipients.getOrInsertFromDistributionListId(DistributionListId.MY_STORY))
recipients = (0 until 5).map { SignalDatabase.recipients.getOrInsertFromServiceId(ACI.from(UUID.randomUUID())) }
releaseChannelRecipient = Recipient.resolved(SignalDatabase.recipients.insertReleaseChannelRecipient())
SignalStore.releaseChannel.setReleaseChannelRecipientId(releaseChannelRecipient.id)
SignalStore.releaseChannelValues().setReleaseChannelRecipientId(releaseChannelRecipient.id)
}
@Test

View File

@@ -1,239 +0,0 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.database
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.signal.storageservice.protos.groups.Member
import org.signal.storageservice.protos.groups.local.DecryptedMember
import org.thoughtcrime.securesms.mms.IncomingMessage
import org.thoughtcrime.securesms.profiles.ProfileName
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.testing.GroupTestingUtils
import org.thoughtcrime.securesms.testing.SignalActivityRule
import org.thoughtcrime.securesms.testing.assertIsSize
@RunWith(AndroidJUnit4::class)
class NameCollisionTablesTest {
@get:Rule
val harness = SignalActivityRule(createGroup = true)
private lateinit var alice: RecipientId
private lateinit var bob: RecipientId
private lateinit var charlie: RecipientId
@Before
fun setUp() {
alice = setUpRecipient(harness.others[0])
bob = setUpRecipient(harness.others[1])
charlie = setUpRecipient(harness.others[2])
}
@Test
fun givenAUserWithAThreadIdButNoConflicts_whenIGetCollisionsForThreadRecipient_thenIExpectNoCollisions() {
val threadRecipientId = alice
SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(threadRecipientId))
val actual = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(threadRecipientId)
actual assertIsSize 0
}
@Test
fun givenTwoUsers_whenOneChangesTheirProfileNameToMatchTheOther_thenIExpectANameCollision() {
setProfileName(alice, ProfileName.fromParts("Alice", "Android"))
setProfileName(bob, ProfileName.fromParts("Bob", "Android"))
setProfileName(alice, ProfileName.fromParts("Bob", "Android"))
val actualAlice = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(alice)
val actualBob = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(bob)
actualAlice assertIsSize 2
actualBob assertIsSize 2
}
@Test
fun givenTwoUsersWithANameCollisions_whenOneChangesToADifferentName_thenIExpectNoNameCollisions() {
setProfileName(alice, ProfileName.fromParts("Bob", "Android"))
setProfileName(bob, ProfileName.fromParts("Bob", "Android"))
setProfileName(alice, ProfileName.fromParts("Alice", "Android"))
val actualAlice = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(alice)
val actualBob = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(bob)
actualAlice assertIsSize 0
actualBob assertIsSize 0
}
@Test
fun givenThreeUsersWithANameCollisions_whenOneChangesToADifferentName_thenIExpectTwoNameCollisions() {
setProfileName(alice, ProfileName.fromParts("Bob", "Android"))
setProfileName(bob, ProfileName.fromParts("Bob", "Android"))
setProfileName(charlie, ProfileName.fromParts("Bob", "Android"))
setProfileName(alice, ProfileName.fromParts("Alice", "Android"))
val actualAlice = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(alice)
val actualBob = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(bob)
val actualCharlie = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(charlie)
actualAlice assertIsSize 0
actualBob assertIsSize 2
actualCharlie assertIsSize 2
}
@Test
fun givenTwoUsersWithADismissedNameCollision_whenOneChangesToADifferentNameAndBack_thenIExpectANameCollision() {
setProfileName(alice, ProfileName.fromParts("Bob", "Android"))
setProfileName(bob, ProfileName.fromParts("Bob", "Android"))
SignalDatabase.nameCollisions.markCollisionsForThreadRecipientDismissed(alice)
setProfileName(alice, ProfileName.fromParts("Alice", "Android"))
setProfileName(alice, ProfileName.fromParts("Bob", "Android"))
val actualAlice = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(alice)
actualAlice assertIsSize 2
}
@Test
fun givenADismissedNameCollisionForAlice_whenIGetNameCollisionsForAlice_thenIExpectNoNameCollisions() {
setProfileName(alice, ProfileName.fromParts("Bob", "Android"))
setProfileName(bob, ProfileName.fromParts("Bob", "Android"))
SignalDatabase.nameCollisions.markCollisionsForThreadRecipientDismissed(alice)
val actualCollisions = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(alice)
actualCollisions assertIsSize 0
}
@Test
fun givenADismissedNameCollisionForAliceThatIUpdate_whenIGetNameCollisionsForAlice_thenIExpectNoNameCollisions() {
SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(alice))
setProfileName(alice, ProfileName.fromParts("Bob", "Android"))
setProfileName(bob, ProfileName.fromParts("Bob", "Android"))
SignalDatabase.nameCollisions.markCollisionsForThreadRecipientDismissed(alice)
setProfileName(bob, ProfileName.fromParts("Bob", "Android"))
val actualCollisions = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(alice)
actualCollisions assertIsSize 0
}
@Test
fun givenADismissedNameCollisionForAlice_whenIGetNameCollisionsForBob_thenIExpectANameCollisionWithTwoEntries() {
SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(alice))
setProfileName(alice, ProfileName.fromParts("Bob", "Android"))
setProfileName(bob, ProfileName.fromParts("Bob", "Android"))
SignalDatabase.nameCollisions.markCollisionsForThreadRecipientDismissed(alice)
val actualCollisions = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(bob)
actualCollisions assertIsSize 2
}
@Test
fun givenAGroupWithAliceAndBob_whenIInsertNameChangeMessageForAlice_thenIExpectAGroupNameCollision() {
val alice = Recipient.resolved(alice)
val bob = Recipient.resolved(bob)
val info = createGroup()
setProfileName(alice.id, ProfileName.fromParts("Bob", "Android"))
setProfileName(bob.id, ProfileName.fromParts("Bob", "Android"))
SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(info.recipientId))
SignalDatabase.messages.insertProfileNameChangeMessages(alice, "Bob Android", "Alice Android")
val collisions = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(info.recipientId)
collisions assertIsSize 2
}
@Test
fun givenAGroupWithAliceAndBobWithDismissedCollision_whenIInsertNameChangeMessageForAlice_thenIExpectAGroupNameCollision() {
val alice = Recipient.resolved(alice)
val bob = Recipient.resolved(bob)
val info = createGroup()
setProfileName(alice.id, ProfileName.fromParts("Bob", "Android"))
setProfileName(bob.id, ProfileName.fromParts("Bob", "Android"))
SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(info.recipientId))
SignalDatabase.messages.insertProfileNameChangeMessages(alice, "Bob Android", "Alice Android")
SignalDatabase.nameCollisions.markCollisionsForThreadRecipientDismissed(info.recipientId)
SignalDatabase.messages.insertProfileNameChangeMessages(alice, "Bob Android", "Alice Android")
val collisions = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(info.recipientId)
collisions assertIsSize 0
}
@Test
fun givenAGroupWithAliceAndBob_whenIInsertNameChangeMessageForAliceWithMismatch_thenIExpectNoGroupNameCollision() {
val alice = Recipient.resolved(alice)
val bob = Recipient.resolved(bob)
val info = createGroup()
setProfileName(alice.id, ProfileName.fromParts("Alice", "Android"))
setProfileName(bob.id, ProfileName.fromParts("Bob", "Android"))
SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(info.recipientId))
SignalDatabase.messages.insertProfileNameChangeMessages(alice, "Alice Android", "Bob Android")
val collisions = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(info.recipientId)
collisions assertIsSize 0
}
private fun setUpRecipient(recipientId: RecipientId): RecipientId {
SignalDatabase.recipients.setProfileSharing(recipientId, false)
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(recipientId, false)
MmsHelper.insert(
threadId = threadId,
message = IncomingMessage(
type = MessageType.NORMAL,
from = recipientId,
groupId = null,
body = "hi",
sentTimeMillis = 100L,
receivedTimeMillis = 200L,
serverTimeMillis = 100L,
isUnidentified = true
)
)
return recipientId
}
private fun setProfileName(recipientId: RecipientId, name: ProfileName) {
SignalDatabase.recipients.setProfileName(recipientId, name)
SignalDatabase.nameCollisions.handleIndividualNameCollision(recipientId)
}
private fun createGroup(): GroupTestingUtils.TestGroupInfo {
return GroupTestingUtils.insertGroup(
revision = 0,
DecryptedMember(
aciBytes = harness.self.requireAci().toByteString(),
role = Member.Role.ADMINISTRATOR
),
DecryptedMember(
aciBytes = Recipient.resolved(alice).requireAci().toByteString(),
role = Member.Role.ADMINISTRATOR
),
DecryptedMember(
aciBytes = Recipient.resolved(bob).requireAci().toByteString(),
role = Member.Role.ADMINISTRATOR
)
)
}
}

View File

@@ -120,7 +120,7 @@ class OneTimePreKeyTableTest {
val count = SignalDatabase.rawDatabase
.update(OneTimePreKeyTable.TABLE_NAME)
.values(OneTimePreKeyTable.STALE_TIMESTAMP to staleTime)
.where("${OneTimePreKeyTable.ACCOUNT_ID} = ? AND ${OneTimePreKeyTable.KEY_ID} = $id", account.toAccountId())
.where("${OneTimePreKeyTable.ACCOUNT_ID} = ? AND ${OneTimePreKeyTable.KEY_ID} = $id", account)
.run()
assertEquals(1, count)
@@ -130,15 +130,8 @@ class OneTimePreKeyTableTest {
return SignalDatabase.rawDatabase
.select(OneTimePreKeyTable.STALE_TIMESTAMP)
.from(OneTimePreKeyTable.TABLE_NAME)
.where("${OneTimePreKeyTable.ACCOUNT_ID} = ? AND ${OneTimePreKeyTable.KEY_ID} = $id", account.toAccountId())
.where("${OneTimePreKeyTable.ACCOUNT_ID} = ? AND ${OneTimePreKeyTable.KEY_ID} = $id", account)
.run()
.readToSingleObject { it.requireLongOrNull(OneTimePreKeyTable.STALE_TIMESTAMP) }
}
private fun ServiceId.toAccountId(): String {
return when (this) {
is ACI -> this.toString()
is PNI -> OneTimePreKeyTable.PNI_ACCOUNT_ID
}
}
}

View File

@@ -11,6 +11,8 @@ import org.signal.core.util.CursorUtil
import org.thoughtcrime.securesms.profiles.ProfileName
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.testing.SignalActivityRule
import org.thoughtcrime.securesms.util.FeatureFlags
import org.thoughtcrime.securesms.util.FeatureFlagsAccessor
import org.whispersystems.signalservice.api.push.ServiceId.ACI
import org.whispersystems.signalservice.api.push.ServiceId.PNI
import java.util.UUID
@@ -57,7 +59,7 @@ class RecipientTableTest {
SignalDatabase.recipients.setProfileName(hiddenRecipient, ProfileName.fromParts("Hidden", "Person"))
SignalDatabase.recipients.markHidden(hiddenRecipient)
val results = SignalDatabase.recipients.querySignalContacts(RecipientTable.ContactSearchQuery("Hidden", false))!!
val results = SignalDatabase.recipients.querySignalContacts("Hidden", false)!!
assertEquals(0, results.count)
}
@@ -128,7 +130,7 @@ class RecipientTableTest {
SignalDatabase.recipients.setProfileName(blockedRecipient, ProfileName.fromParts("Blocked", "Person"))
SignalDatabase.recipients.setBlocked(blockedRecipient, true)
val results = SignalDatabase.recipients.querySignalContacts(RecipientTable.ContactSearchQuery("Blocked", false))!!
val results = SignalDatabase.recipients.querySignalContacts("Blocked", false)!!
assertEquals(0, results.count)
}
@@ -165,6 +167,8 @@ class RecipientTableTest {
@Test
fun givenARecipientWithPniAndAci_whenIMarkItUnregistered_thenIExpectItToBeSplit() {
FeatureFlagsAccessor.forceValue(FeatureFlags.PHONE_NUMBER_PRIVACY, true)
val mainId = SignalDatabase.recipients.getAndPossiblyMerge(ACI_A, PNI_A, E164_A)
SignalDatabase.recipients.markUnregistered(mainId)
@@ -181,6 +185,8 @@ class RecipientTableTest {
@Test
fun givenARecipientWithPniAndAci_whenISplitItForStorageSync_thenIExpectItToBeSplit() {
FeatureFlagsAccessor.forceValue(FeatureFlags.PHONE_NUMBER_PRIVACY, true)
val mainId = SignalDatabase.recipients.getAndPossiblyMerge(ACI_A, PNI_A, E164_A)
val mainRecord = SignalDatabase.recipients.getRecord(mainId)

View File

@@ -9,7 +9,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.storage.StorageRecordUpdate
import org.thoughtcrime.securesms.storage.StorageSyncModels
@@ -28,7 +28,7 @@ class RecipientTableTest_applyStorageSyncContactUpdate {
@Test
fun insertMessageOnVerifiedToDefault() {
// GIVEN
val identities = AppDependencies.protocolStore.aci().identities()
val identities = ApplicationDependencies.getProtocolStore().aci().identities()
val other = Recipient.resolved(harness.others[0])
MmsHelper.insert(recipient = other)

View File

@@ -34,13 +34,15 @@ import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.database.model.ReactionRecord
import org.thoughtcrime.securesms.database.model.databaseprotos.SessionSwitchoverEvent
import org.thoughtcrime.securesms.database.model.databaseprotos.ThreadMergeEvent
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.keyvalue.SignalStore
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.FeatureFlags
import org.thoughtcrime.securesms.util.FeatureFlagsAccessor
import org.thoughtcrime.securesms.util.Util
import org.whispersystems.signalservice.api.push.ServiceId.ACI
import org.whispersystems.signalservice.api.push.ServiceId.PNI
@@ -53,9 +55,10 @@ class RecipientTableTest_getAndPossiblyMerge {
@Before
fun setup() {
SignalStore.account.setE164(E164_SELF)
SignalStore.account.setAci(ACI_SELF)
SignalStore.account.setPni(PNI_SELF)
SignalStore.account().setE164(E164_SELF)
SignalStore.account().setAci(ACI_SELF)
SignalStore.account().setPni(PNI_SELF)
FeatureFlagsAccessor.forceValue(FeatureFlags.PHONE_NUMBER_PRIVACY, true)
}
@Test
@@ -776,18 +779,6 @@ class RecipientTableTest_getAndPossiblyMerge {
expectThreadMergeEvent(E164_A)
}
test("merge, e164+pni & e164+aci, pni+aci provided, change number") {
given(E164_A, PNI_A, null)
given(E164_B, null, ACI_A)
process(null, PNI_A, ACI_A)
expect(E164_A, PNI_A, ACI_A)
expectThreadMergeEvent(E164_A)
expectChangeNumberEvent()
}
test("merge, e164 + pni reassigned, aci abandoned") {
given(E164_A, PNI_A, ACI_A)
given(E164_B, PNI_B, ACI_B)
@@ -800,17 +791,6 @@ class RecipientTableTest_getAndPossiblyMerge {
expectChangeNumberEvent()
}
test("merge, e164 follows pni+aci") {
given(E164_A, PNI_A, null)
given(null, null, ACI_A)
process(null, PNI_A, ACI_A, pniVerified = true)
expect(E164_A, PNI_A, ACI_A)
expectThreadMergeEvent(E164_A)
expectPniVerified()
}
test("local user, local e164 and aci provided, changeSelf=false, leave e164 alone") {
given(E164_SELF, null, ACI_SELF)
given(null, null, ACI_A)
@@ -1103,8 +1083,8 @@ class RecipientTableTest_getAndPossiblyMerge {
init {
// Need to delete these first to prevent foreign key crash
SignalDatabase.rawDatabase.execSQL("DELETE FROM ${DistributionListTables.ListTable.TABLE_NAME}")
SignalDatabase.rawDatabase.execSQL("DELETE FROM ${DistributionListTables.MembershipTable.TABLE_NAME}")
SignalDatabase.rawDatabase.execSQL("DELETE FROM distribution_list")
SignalDatabase.rawDatabase.execSQL("DELETE FROM distribution_list_member")
SqlUtil.getAllTables(SignalDatabase.rawDatabase)
.filterNot { it.contains("sqlite") || it.contains("fts") || it.startsWith("emoji_search_") } // If we delete these we'll corrupt the DB
@@ -1113,8 +1093,8 @@ class RecipientTableTest_getAndPossiblyMerge {
SignalDatabase.rawDatabase.execSQL("DELETE FROM $table")
}
AppDependencies.recipientCache.clear()
AppDependencies.recipientCache.clearSelf()
ApplicationDependencies.getRecipientCache().clear()
ApplicationDependencies.getRecipientCache().clearSelf()
RecipientId.clearCache()
}

View File

@@ -4,12 +4,9 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import junit.framework.Assert.assertFalse
import junit.framework.Assert.assertTrue
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.signal.core.util.concurrent.SignalExecutors
import org.thoughtcrime.securesms.testing.SignalFlakyTest
import org.thoughtcrime.securesms.testing.SignalFlakyTestRule
import java.util.concurrent.CountDownLatch
import java.util.concurrent.atomic.AtomicBoolean
@@ -21,9 +18,6 @@ class SQLiteDatabaseTest {
private lateinit var db: SQLiteDatabase
@get:Rule
val flakyTestRule = SignalFlakyTestRule()
@Before
fun setup() {
db = SignalDatabase.instance!!.signalWritableDatabase
@@ -187,7 +181,6 @@ class SQLiteDatabaseTest {
assertTrue(hasRun2.get())
}
@SignalFlakyTest
@Test
fun runPostSuccessfulTransaction_runsAfterMainTransactionInNestedTransaction() {
val hasRun1 = AtomicBoolean(false)

View File

@@ -10,9 +10,7 @@ import org.junit.Test
import org.junit.runner.RunWith
import org.signal.core.util.Hex
import org.signal.libsignal.zkgroup.groups.GroupMasterKey
import org.thoughtcrime.securesms.database.model.GroupsV2UpdateMessageConverter
import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context
import org.thoughtcrime.securesms.database.model.databaseprotos.GV2UpdateDescription
import org.thoughtcrime.securesms.database.model.databaseprotos.addMember
import org.thoughtcrime.securesms.database.model.databaseprotos.addRequestingMember
import org.thoughtcrime.securesms.database.model.databaseprotos.deleteRequestingMember
@@ -46,8 +44,8 @@ class SmsDatabaseTest_collapseJoinRequestEventsIfPossible {
recipients = SignalDatabase.recipients
sms = SignalDatabase.messages
SignalStore.account.setAci(localAci)
SignalStore.account.setPni(localPni)
SignalStore.account().setAci(localAci)
SignalStore.account().setPni(localPni)
alice = recipients.getOrInsertFromServiceId(aliceServiceId)
bob = recipients.getOrInsertFromServiceId(bobServiceId)
@@ -288,18 +286,11 @@ class SmsDatabaseTest_collapseJoinRequestEventsIfPossible {
private fun groupUpdateMessage(sender: RecipientId, groupContext: DecryptedGroupV2Context): IncomingMessage {
wallClock++
val updateDescription = GV2UpdateDescription(
gv2ChangeDescription = groupContext,
groupChangeUpdate = GroupsV2UpdateMessageConverter.translateDecryptedChangeUpdate(SignalStore.account.getServiceIds(), groupContext)
)
return IncomingMessage.groupUpdate(
from = sender,
timestamp = wallClock,
groupId = groupId,
update = updateDescription,
isGroupAdd = false,
groupContext = groupContext,
serverGuid = null
)
}

View File

@@ -5,7 +5,6 @@ import org.thoughtcrime.securesms.attachments.UriAttachment
import org.thoughtcrime.securesms.audio.AudioHash
import org.thoughtcrime.securesms.blurhash.BlurHash
import org.thoughtcrime.securesms.stickers.StickerLocator
import java.util.UUID
object UriAttachmentBuilder {
fun build(
@@ -23,28 +22,23 @@ object UriAttachmentBuilder {
stickerLocator: StickerLocator? = null,
blurHash: BlurHash? = null,
audioHash: AudioHash? = null,
transformProperties: AttachmentTable.TransformProperties? = null,
uuid: UUID? = UUID.randomUUID()
transformProperties: AttachmentTable.TransformProperties? = null
): UriAttachment {
return UriAttachment(
dataUri = uri,
contentType = contentType,
transferState = transferState,
size = size,
width = 0,
height = 0,
fileName = fileName,
fastPreflightId = null,
voiceNote = voiceNote,
borderless = borderless,
videoGif = videoGif,
quote = quote,
caption = caption,
stickerLocator = stickerLocator,
blurHash = blurHash,
audioHash = audioHash,
transformProperties = transformProperties,
uuid = uuid
uri,
contentType,
transferState,
size,
fileName,
voiceNote,
borderless,
videoGif,
quote,
caption,
stickerLocator,
blurHash,
audioHash,
transformProperties
)
}
}

View File

@@ -1,148 +0,0 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.database.helpers.migration
import android.app.Application
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.signal.core.util.readToSingleObject
import org.signal.core.util.requireNonNullString
import org.signal.core.util.select
import org.signal.core.util.update
import org.signal.donations.InAppPaymentType
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationSerializationHelper.toDecimalValue
import org.thoughtcrime.securesms.database.InAppPaymentSubscriberTable
import org.thoughtcrime.securesms.database.InAppPaymentTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
import org.thoughtcrime.securesms.database.model.databaseprotos.FiatValue
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
import org.thoughtcrime.securesms.testing.SignalDatabaseRule
import org.thoughtcrime.securesms.testing.assertIs
import org.whispersystems.signalservice.api.subscriptions.SubscriberId
import java.math.BigDecimal
import java.util.Currency
@RunWith(AndroidJUnit4::class)
class FixInAppCurrencyIfAbleTest {
@get:Rule
val harness = SignalDatabaseRule(deleteAllThreadsOnEachRun = false)
@Test
fun givenNoSubscribers_whenIMigrate_thenIDoNothing() {
migrate()
}
@Test
fun givenASubscriberButNoPayment_whenIMigrate_thenIDoNothing() {
val subscriber = insertSubscriber("USD")
clearCurrencyCode(subscriber)
migrate()
getCurrencyCode(subscriber) assertIs ""
}
@Test
fun givenASubscriberAndMismatchedPayment_whenIMigrate_thenIDoNothing() {
val subscriber = insertSubscriber("USD")
val otherSubscriber = insertSubscriber("EUR")
insertPayment(otherSubscriber)
clearCurrencyCode(subscriber)
migrate()
getCurrencyCode(subscriber) assertIs ""
}
@Test
fun givenASubscriberAndPaymentWithNoSubscriber_whenIMigrate_thenDoNothing() {
val subscriber = insertSubscriber("USD")
insertPayment(null)
clearCurrencyCode(subscriber)
migrate()
getCurrencyCode(subscriber) assertIs ""
}
@Test
fun givenASubscriberAndMatchingPayment_whenIMigrate_thenUpdateCurrencyCode() {
val subscriber = insertSubscriber("USD")
insertPayment(subscriber)
clearCurrencyCode(subscriber)
migrate()
getCurrencyCode(subscriber) assertIs "USD"
}
@Test
fun givenASupercededSubscriber_whenIMigrate_thenIDoNothing() {
val oldSubscriber = insertSubscriber("USD")
insertPayment(oldSubscriber)
clearCurrencyCode(oldSubscriber)
insertSubscriber("USD")
migrate()
}
private fun migrate() {
V236_FixInAppSubscriberCurrencyIfAble.migrate(
context = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as Application,
db = SignalDatabase.rawDatabase,
oldVersion = 0,
newVersion = 0
)
}
private fun insertSubscriber(currencyCode: String): InAppPaymentSubscriberRecord {
val record = InAppPaymentSubscriberRecord(
subscriberId = SubscriberId.generate(),
currency = Currency.getInstance(currencyCode),
type = InAppPaymentSubscriberRecord.Type.DONATION,
requiresCancel = false,
paymentMethodType = InAppPaymentData.PaymentMethodType.PAYPAL
)
SignalDatabase.inAppPaymentSubscribers.insertOrReplace(record)
return record
}
private fun clearCurrencyCode(inAppPaymentSubscriberRecord: InAppPaymentSubscriberRecord) {
SignalDatabase.rawDatabase.update(InAppPaymentSubscriberTable.TABLE_NAME)
.values(InAppPaymentSubscriberTable.CURRENCY_CODE to "")
.where("${InAppPaymentSubscriberTable.SUBSCRIBER_ID} = ?", inAppPaymentSubscriberRecord.subscriberId.serialize())
.run()
}
private fun getCurrencyCode(inAppPaymentSubscriberRecord: InAppPaymentSubscriberRecord): String {
return SignalDatabase.rawDatabase.select(InAppPaymentSubscriberTable.CURRENCY_CODE)
.from(InAppPaymentSubscriberTable.TABLE_NAME)
.where("${InAppPaymentSubscriberTable.SUBSCRIBER_ID} = ?", inAppPaymentSubscriberRecord.subscriberId.serialize())
.run()
.readToSingleObject { it.requireNonNullString(InAppPaymentSubscriberTable.CURRENCY_CODE) }!!
}
private fun insertPayment(inAppPaymentSubscriberRecord: InAppPaymentSubscriberRecord?): InAppPaymentTable.InAppPayment {
val id = SignalDatabase.inAppPayments.insert(
type = InAppPaymentType.RECURRING_DONATION,
state = InAppPaymentTable.State.END,
subscriberId = inAppPaymentSubscriberRecord?.subscriberId,
endOfPeriod = null,
inAppPaymentData = InAppPaymentData(
amount = FiatValue(
currencyCode = inAppPaymentSubscriberRecord?.currency?.currencyCode ?: "USD",
amount = BigDecimal.ONE.toDecimalValue()
),
level = 200,
paymentMethodType = inAppPaymentSubscriberRecord?.paymentMethodType ?: InAppPaymentData.PaymentMethodType.UNKNOWN
)
)
return SignalDatabase.inAppPayments.getById(id)!!
}
}

View File

@@ -1,7 +1,6 @@
package org.thoughtcrime.securesms.dependencies
import android.app.Application
import io.mockk.spyk
import okhttp3.ConnectionSpec
import okhttp3.Response
import okhttp3.WebSocket
@@ -24,9 +23,6 @@ import org.thoughtcrime.securesms.testing.Get
import org.thoughtcrime.securesms.testing.Verb
import org.thoughtcrime.securesms.testing.runSync
import org.thoughtcrime.securesms.testing.success
import org.whispersystems.signalservice.api.SignalServiceDataStore
import org.whispersystems.signalservice.api.SignalServiceMessageSender
import org.whispersystems.signalservice.api.SignalWebSocket
import org.whispersystems.signalservice.api.push.TrustStore
import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl
import org.whispersystems.signalservice.internal.configuration.SignalCdsiUrl
@@ -41,13 +37,12 @@ import java.util.Optional
*
* Handles setting up a mock web server for API calls, and provides mockable versions of [SignalServiceNetworkAccess].
*/
class InstrumentationApplicationDependencyProvider(val application: Application, private val default: ApplicationDependencyProvider) : AppDependencies.Provider by default {
class InstrumentationApplicationDependencyProvider(application: Application, default: ApplicationDependencyProvider) : ApplicationDependencies.Provider by default {
private val serviceTrustStore: TrustStore
private val uncensoredConfiguration: SignalServiceConfiguration
private val serviceNetworkAccessMock: SignalServiceNetworkAccess
private val recipientCache: LiveRecipientCache
private var signalServiceMessageSender: SignalServiceMessageSender? = null
init {
runSync {
@@ -58,21 +53,18 @@ class InstrumentationApplicationDependencyProvider(val application: Application,
Get("/v1/websocket/?login=") {
MockResponse().success().withWebSocketUpgrade(mockIdentifiedWebSocket)
},
Get("/v1/websocket", {
val path = it.path
return@Get path == null || !path.contains("login")
}) {
Get("/v1/websocket", { !it.path.contains("login") }) {
MockResponse().success().withWebSocketUpgrade(object : WebSocketListener() {})
}
)
}
webServer.dispatcher = object : Dispatcher() {
webServer.setDispatcher(object : Dispatcher() {
override fun dispatch(request: RecordedRequest): MockResponse {
val handler = handlers.firstOrNull { it.requestPredicate(request) }
return handler?.responseFactory?.invoke(request) ?: MockResponse().setResponseCode(500)
}
}
})
serviceTrustStore = SignalServiceTrustStore(application)
uncensoredConfiguration = SignalServiceConfiguration(
@@ -109,17 +101,6 @@ class InstrumentationApplicationDependencyProvider(val application: Application,
return recipientCache
}
override fun provideSignalServiceMessageSender(
signalWebSocket: SignalWebSocket,
protocolStore: SignalServiceDataStore,
signalServiceConfiguration: SignalServiceConfiguration
): SignalServiceMessageSender {
if (signalServiceMessageSender == null) {
signalServiceMessageSender = spyk(objToCopy = default.provideSignalServiceMessageSender(signalWebSocket, protocolStore, signalServiceConfiguration))
}
return signalServiceMessageSender!!
}
class MockWebSocket : WebSocketListener() {
private val TAG = "MockWebSocket"

View File

@@ -16,7 +16,7 @@ import org.thoughtcrime.securesms.attachments.UriAttachment
import org.thoughtcrime.securesms.database.AttachmentTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.UriAttachmentBuilder
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.jobmanager.Job
import org.thoughtcrime.securesms.mms.SentMediaQuality
import org.thoughtcrime.securesms.providers.BlobProvider
@@ -38,7 +38,7 @@ class AttachmentCompressionJobTest {
StreamUtil.readFully(it)
}
val blob = BlobProvider.getInstance().forData(imageBytes).createForSingleSessionOnDisk(AppDependencies.application)
val blob = BlobProvider.getInstance().forData(imageBytes).createForSingleSessionOnDisk(ApplicationDependencies.getApplication())
val firstPreUpload = createAttachment(1, blob, AttachmentTable.TransformProperties.empty())
val firstDatabaseAttachment = SignalDatabase.attachments.insertAttachmentForPreUpload(firstPreUpload)
@@ -51,12 +51,12 @@ class AttachmentCompressionJobTest {
val secondJobLatch = CountDownLatch(1)
val jobThread = Thread {
firstCompressionJob.setContext(AppDependencies.application)
firstCompressionJob.setContext(ApplicationDependencies.getApplication())
firstJobResult = firstCompressionJob.run()
secondJobLatch.await()
secondCompressionJob!!.setContext(AppDependencies.application)
secondCompressionJob!!.setContext(ApplicationDependencies.getApplication())
secondJobResult = secondCompressionJob!!.run()
}

View File

@@ -1,117 +0,0 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.jobs
import android.app.Application
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.signal.core.util.EventTimer
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.database.JobDatabase.Companion.getInstance
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.jobmanager.Job
import org.thoughtcrime.securesms.jobmanager.JobManager
import org.thoughtcrime.securesms.jobmanager.JobMigrator
import org.thoughtcrime.securesms.jobmanager.JobTracker
import org.thoughtcrime.securesms.util.TextSecurePreferences
import java.util.concurrent.CountDownLatch
import kotlin.random.Random
@Ignore("This is just for testing performance, not correctness, and they can therefore take a long time. Run them manually when you need to.")
@RunWith(AndroidJUnit4::class)
class JobManagerPerformanceTests {
companion object {
val TAG = Log.tag(JobManagerPerformanceTests::class.java)
}
@Test
fun testPerformance_singleQueue() {
runTest("singleQueue", 2000) { TestJob(queue = "queue") }
}
@Test
fun testPerformance_fourQueues() {
runTest("fourQueues", 2000) { TestJob(queue = "queue-${Random.nextInt(1, 5)}") }
}
@Test
fun testPerformance_noQueues() {
runTest("noQueues", 2000) { TestJob(queue = null) }
}
private fun runTest(name: String, count: Int, jobCreator: () -> TestJob) {
val context = AppDependencies.application
val jobManager = testJobManager(context)
jobManager.beginJobLoop()
val eventTimer = EventTimer()
val latch = CountDownLatch(count)
var seenStart = false
jobManager.addListener({ it.factoryKey == TestJob.KEY }) { _, state ->
if (!seenStart && state == JobTracker.JobState.RUNNING) {
// Adding the jobs can take a while (and runs on a background thread), so we want to reset the timer the first time we see a job run so the first job
// doesn't have a skewed time
eventTimer.reset()
seenStart = true
}
if (state.isComplete) {
eventTimer.emit("job")
latch.countDown()
if (latch.count % 100 == 0L) {
Log.d(TAG, "[$name] Finished ${count - latch.count}/$count jobs")
}
}
}
Log.i(TAG, "[$name] Adding jobs...")
jobManager.addAll((1..count).map { jobCreator() })
Log.i(TAG, "[$name] Waiting for jobs to complete...")
latch.await()
Log.i(TAG, "[$name] Jobs complete!")
Log.i(TAG, eventTimer.stop().summary)
}
private fun testJobManager(context: Application): JobManager {
val config = JobManager.Configuration.Builder()
.setJobFactories(
JobManagerFactories.getJobFactories(context) + mapOf(
TestJob.KEY to TestJob.Factory()
)
)
.setConstraintFactories(JobManagerFactories.getConstraintFactories(context))
.setConstraintObservers(JobManagerFactories.getConstraintObservers(context))
.setJobStorage(FastJobStorage(getInstance(context)))
.setJobMigrator(JobMigrator(TextSecurePreferences.getJobManagerVersion(context), JobManager.CURRENT_VERSION, JobManagerFactories.getJobMigrations(context)))
.build()
return JobManager(context, config)
}
private class TestJob(params: Parameters) : Job(params) {
companion object {
const val KEY = "test"
}
constructor(queue: String?) : this(Parameters.Builder().setQueue(queue).build())
override fun serialize(): ByteArray? = null
override fun getFactoryKey(): String = KEY
override fun run(): Result = Result.success()
override fun onFailure() = Unit
class Factory : Job.Factory<TestJob> {
override fun create(parameters: Parameters, serializedData: ByteArray?): TestJob {
return TestJob(parameters)
}
}
}
}

View File

@@ -52,7 +52,7 @@ class MessageContentProcessor__recipientStatusTest {
processor.process(
envelope = MessageContentFuzzer.envelope(envelopeTimestamp),
content = MessageContentFuzzer.syncSentTextMessage(initialTextMessage, deliveredTo = listOf(harness.others[0])),
metadata = MessageContentFuzzer.envelopeMetadata(harness.self.id, harness.self.id, groupId = groupId),
metadata = MessageContentFuzzer.envelopeMetadata(harness.self.id, harness.self.id, groupId),
serverDeliveredTimestamp = MessageContentFuzzer.fuzzServerDeliveredTimestamp(envelopeTimestamp)
)
@@ -64,7 +64,7 @@ class MessageContentProcessor__recipientStatusTest {
processor.process(
envelope = MessageContentFuzzer.envelope(envelopeTimestamp),
content = MessageContentFuzzer.syncSentTextMessage(initialTextMessage, deliveredTo = listOf(harness.others[0], harness.others[1]), recipientUpdate = true),
metadata = MessageContentFuzzer.envelopeMetadata(harness.self.id, harness.self.id, groupId = groupId),
metadata = MessageContentFuzzer.envelopeMetadata(harness.self.id, harness.self.id, groupId),
serverDeliveredTimestamp = MessageContentFuzzer.fuzzServerDeliveredTimestamp(envelopeTimestamp)
)

View File

@@ -1,290 +0,0 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.messages
import android.net.Uri
import io.mockk.every
import io.mockk.mockkStatic
import io.mockk.slot
import io.mockk.unmockkStatic
import org.thoughtcrime.securesms.attachments.Attachment
import org.thoughtcrime.securesms.attachments.UriAttachment
import org.thoughtcrime.securesms.database.AttachmentTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.UriAttachmentBuilder
import org.thoughtcrime.securesms.database.model.GroupsV2UpdateMessageConverter
import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context
import org.thoughtcrime.securesms.database.model.databaseprotos.GV2UpdateDescription
import org.thoughtcrime.securesms.jobs.ThreadUpdateJob
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.mms.OutgoingMessage
import org.thoughtcrime.securesms.providers.BlobProvider
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.testing.GroupTestingUtils
import org.thoughtcrime.securesms.testing.MessageContentFuzzer
import org.thoughtcrime.securesms.testing.SignalActivityRule
import org.thoughtcrime.securesms.util.MediaUtil
import java.util.UUID
import kotlin.random.Random
/**
* Makes inserting messages through the "normal" code paths simpler. Mostly focused on incoming messages.
*/
class MessageHelper(private val harness: SignalActivityRule, var startTime: Long = System.currentTimeMillis()) {
val alice: RecipientId = harness.others[0]
val bob: RecipientId = harness.others[1]
val group: GroupTestingUtils.TestGroupInfo = harness.group!!
val processor: MessageContentProcessor = MessageContentProcessor(harness.context)
init {
val threadIdSlot = slot<Long>()
mockkStatic(ThreadUpdateJob::class)
every { ThreadUpdateJob.enqueue(capture(threadIdSlot)) } answers {
SignalDatabase.threads.update(threadIdSlot.captured, false)
}
}
fun tearDown() {
unmockkStatic(ThreadUpdateJob::class)
}
fun incomingText(sender: RecipientId = alice, destination: RecipientId = harness.self.id): MessageData {
startTime = nextStartTime()
val messageData = MessageData(author = sender, timestamp = startTime)
processor.process(
envelope = MessageContentFuzzer.envelope(messageData.timestamp, serverGuid = messageData.serverGuid),
content = MessageContentFuzzer.fuzzTextMessage(
sentTimestamp = messageData.timestamp,
groupContextV2 = if (destination == group.recipientId) group.groupV2Context else null,
allowExpireTimeChanges = false
),
metadata = MessageContentFuzzer.envelopeMetadata(
source = sender,
destination = harness.self.id,
groupId = if (destination == group.recipientId) group.groupId else null
),
serverDeliveredTimestamp = messageData.timestamp + 10
)
return messageData
}
fun outgoingText(conversationId: RecipientId = alice, successfulSend: Boolean = true, updateMessage: ((OutgoingMessage) -> OutgoingMessage)? = null): MessageData {
startTime = nextStartTime()
val messageData = MessageData(author = harness.self.id, timestamp = startTime)
val threadRecipient = Recipient.resolved(conversationId)
val message = OutgoingMessage(
threadRecipient = threadRecipient,
body = MessageContentFuzzer.string(),
sentTimeMillis = messageData.timestamp,
isUrgent = true,
isSecure = true
).let { updateMessage?.invoke(it) ?: it }
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(threadRecipient)
val messageId = SignalDatabase.messages.insertMessageOutbox(message, threadId, false, null)
if (successfulSend) {
SignalDatabase.messages.markAsSent(messageId, true)
}
return messageData.copy(messageId = messageId)
}
fun outgoingMessage(conversationId: RecipientId = alice, updateMessage: OutgoingMessage.() -> OutgoingMessage): MessageData {
startTime = nextStartTime()
val messageData = MessageData(author = harness.self.id, timestamp = startTime)
val threadRecipient = Recipient.resolved(conversationId)
val message = OutgoingMessage(
threadRecipient = threadRecipient,
sentTimeMillis = messageData.timestamp,
isUrgent = true,
isSecure = true
).apply { updateMessage() }
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(threadRecipient)
val messageId = SignalDatabase.messages.insertMessageOutbox(message, threadId, false, null)
return messageData.copy(messageId = messageId)
}
fun outgoingAttachment(data: ByteArray, uuid: UUID? = UUID.randomUUID()): Attachment {
val uri: Uri = BlobProvider.getInstance().forData(data).createForSingleSessionInMemory()
val attachment: UriAttachment = UriAttachmentBuilder.build(
id = Random.nextLong(),
uri = uri,
contentType = MediaUtil.IMAGE_JPEG,
transformProperties = AttachmentTable.TransformProperties(),
uuid = uuid
)
return attachment
}
fun outgoingGroupChange(): MessageData {
startTime = nextStartTime()
val messageData = MessageData(author = harness.self.id, timestamp = startTime)
val groupRecipient = Recipient.resolved(group.recipientId)
val decryptedGroupV2Context = DecryptedGroupV2Context(
context = group.groupV2Context,
groupState = SignalDatabase.groups.getGroup(group.groupId).get().requireV2GroupProperties().decryptedGroup
)
val updateDescription = GV2UpdateDescription.Builder()
.gv2ChangeDescription(decryptedGroupV2Context)
.groupChangeUpdate(GroupsV2UpdateMessageConverter.translateDecryptedChange(SignalStore.account.getServiceIds(), decryptedGroupV2Context))
.build()
val outgoingMessage = OutgoingMessage.groupUpdateMessage(groupRecipient, updateDescription, startTime)
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(groupRecipient)
val messageId = SignalDatabase.messages.insertMessageOutbox(outgoingMessage, threadId, false, null)
SignalDatabase.messages.markAsSent(messageId, true)
return messageData.copy(messageId = messageId)
}
fun incomingMedia(sender: RecipientId = alice, destination: RecipientId = harness.self.id): MessageData {
startTime = nextStartTime()
val messageData = MessageData(author = sender, timestamp = startTime)
processor.process(
envelope = MessageContentFuzzer.envelope(messageData.timestamp, serverGuid = messageData.serverGuid),
content = MessageContentFuzzer.fuzzStickerMediaMessage(
sentTimestamp = messageData.timestamp,
groupContextV2 = if (destination == group.recipientId) group.groupV2Context else null
),
metadata = MessageContentFuzzer.envelopeMetadata(
source = sender,
destination = harness.self.id,
groupId = if (destination == group.recipientId) group.groupId else null
),
serverDeliveredTimestamp = messageData.timestamp + 10
)
return messageData
}
fun incomingEditText(targetTimestamp: Long = System.currentTimeMillis(), sender: RecipientId = alice, destination: RecipientId = harness.self.id): MessageData {
startTime = nextStartTime()
val messageData = MessageData(author = sender, timestamp = startTime)
processor.process(
envelope = MessageContentFuzzer.envelope(messageData.timestamp, serverGuid = messageData.serverGuid),
content = MessageContentFuzzer.editTextMessage(
targetTimestamp = targetTimestamp,
editedDataMessage = MessageContentFuzzer.fuzzTextMessage(
sentTimestamp = messageData.timestamp,
groupContextV2 = if (destination == group.recipientId) group.groupV2Context else null
).dataMessage!!
),
metadata = MessageContentFuzzer.envelopeMetadata(
source = sender,
destination = harness.self.id,
groupId = if (destination == group.recipientId) group.groupId else null
),
serverDeliveredTimestamp = messageData.timestamp + 10
)
return messageData
}
fun syncReadMessage(vararg reads: Pair<RecipientId, Long>): MessageData {
startTime = nextStartTime()
val messageData = MessageData(timestamp = startTime)
processor.process(
envelope = MessageContentFuzzer.envelope(messageData.timestamp, serverGuid = messageData.serverGuid),
content = MessageContentFuzzer.syncReadsMessage(reads.toList()),
metadata = MessageContentFuzzer.envelopeMetadata(harness.self.id, harness.self.id, sourceDeviceId = 2),
serverDeliveredTimestamp = messageData.timestamp + 10
)
return messageData
}
fun syncDeleteForMeMessage(vararg deletes: MessageContentFuzzer.DeleteForMeSync): MessageData {
startTime = nextStartTime()
val messageData = MessageData(timestamp = startTime)
processor.process(
envelope = MessageContentFuzzer.envelope(messageData.timestamp, serverGuid = messageData.serverGuid),
content = MessageContentFuzzer.syncDeleteForMeMessage(deletes.toList()),
metadata = MessageContentFuzzer.envelopeMetadata(harness.self.id, harness.self.id, sourceDeviceId = 2),
serverDeliveredTimestamp = messageData.timestamp + 10
)
return messageData
}
fun syncDeleteForMeConversation(vararg deletes: MessageContentFuzzer.DeleteForMeSync): MessageData {
startTime = nextStartTime()
val messageData = MessageData(timestamp = startTime)
processor.process(
envelope = MessageContentFuzzer.envelope(messageData.timestamp, serverGuid = messageData.serverGuid),
content = MessageContentFuzzer.syncDeleteForMeConversation(deletes.toList()),
metadata = MessageContentFuzzer.envelopeMetadata(harness.self.id, harness.self.id, sourceDeviceId = 2),
serverDeliveredTimestamp = messageData.timestamp + 10
)
return messageData
}
fun syncDeleteForMeLocalOnlyConversation(vararg conversations: RecipientId): MessageData {
startTime = nextStartTime()
val messageData = MessageData(timestamp = startTime)
processor.process(
envelope = MessageContentFuzzer.envelope(messageData.timestamp, serverGuid = messageData.serverGuid),
content = MessageContentFuzzer.syncDeleteForMeLocalOnlyConversation(conversations.toList()),
metadata = MessageContentFuzzer.envelopeMetadata(harness.self.id, harness.self.id, sourceDeviceId = 2),
serverDeliveredTimestamp = messageData.timestamp + 10
)
return messageData
}
fun syncDeleteForMeAttachment(conversationId: RecipientId, message: Pair<RecipientId, Long>, uuid: UUID?, digest: ByteArray?, plainTextHash: String?): MessageData {
startTime = nextStartTime()
val messageData = MessageData(timestamp = startTime)
processor.process(
envelope = MessageContentFuzzer.envelope(messageData.timestamp, serverGuid = messageData.serverGuid),
content = MessageContentFuzzer.syncDeleteForMeAttachment(conversationId, message, uuid, digest, plainTextHash),
metadata = MessageContentFuzzer.envelopeMetadata(harness.self.id, harness.self.id, sourceDeviceId = 2),
serverDeliveredTimestamp = messageData.timestamp + 10
)
return messageData
}
/**
* Get the next "sentTimestamp" for current + [nextMessageOffset]th message. Useful for early message processing and future message timestamps.
*/
fun nextStartTime(nextMessageOffset: Int = 1): Long {
return startTime + 1000 * nextMessageOffset
}
data class MessageData(
val author: RecipientId = RecipientId.UNKNOWN,
val serverGuid: UUID = UUID.randomUUID(),
val timestamp: Long,
val messageId: Long = -1L
)
}

View File

@@ -16,7 +16,7 @@ import org.signal.core.util.logging.Log
import org.signal.libsignal.protocol.ecc.Curve
import org.signal.libsignal.protocol.ecc.ECKeyPair
import org.signal.libsignal.zkgroup.profiles.ProfileKey
import org.thoughtcrime.securesms.crypto.SealedSenderAccessUtil
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil
import org.thoughtcrime.securesms.dependencies.InstrumentationApplicationDependencyProvider
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.testing.AliceClient
@@ -55,8 +55,8 @@ class MessageProcessingPerformanceTest {
@Before
fun setup() {
mockkStatic(SealedSenderAccessUtil::class)
every { SealedSenderAccessUtil.getCertificateValidator() } returns FakeClientHelpers.noOpCertificateValidator
mockkStatic(UnidentifiedAccessUtil::class)
every { UnidentifiedAccessUtil.getCertificateValidator() } returns FakeClientHelpers.noOpCertificateValidator
mockkObject(MessageContentProcessor)
every { MessageContentProcessor.create(harness.application) } returns TimingMessageContentProcessor(harness.application)
@@ -64,7 +64,7 @@ class MessageProcessingPerformanceTest {
@After
fun after() {
unmockkStatic(SealedSenderAccessUtil::class)
unmockkStatic(UnidentifiedAccessUtil::class)
unmockkStatic(MessageContentProcessor::class)
}

View File

@@ -1,107 +0,0 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.messages
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.testing.SignalActivityRule
import org.thoughtcrime.securesms.testing.assertIs
@Suppress("ClassName")
@RunWith(AndroidJUnit4::class)
class SyncMessageProcessorTest_readSyncs {
@get:Rule
val harness = SignalActivityRule(createGroup = true)
private lateinit var messageHelper: MessageHelper
@Before
fun setUp() {
messageHelper = MessageHelper(harness)
}
@After
fun tearDown() {
messageHelper.tearDown()
}
@Test
fun handleSynchronizeReadMessage() {
val message1Timestamp = messageHelper.incomingText().timestamp
val message2Timestamp = messageHelper.incomingText().timestamp
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.alice)!!
var threadRecord = SignalDatabase.threads.getThreadRecord(threadId)!!
threadRecord.unreadCount assertIs 2
messageHelper.syncReadMessage(messageHelper.alice to message1Timestamp, messageHelper.alice to message2Timestamp)
threadRecord = SignalDatabase.threads.getThreadRecord(threadId)!!
threadRecord.unreadCount assertIs 0
}
@Test
fun handleSynchronizeReadMessageMissingTimestamp() {
messageHelper.incomingText().timestamp
val message2Timestamp = messageHelper.incomingText().timestamp
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.alice)!!
var threadRecord = SignalDatabase.threads.getThreadRecord(threadId)!!
threadRecord.unreadCount assertIs 2
messageHelper.syncReadMessage(messageHelper.alice to message2Timestamp)
threadRecord = SignalDatabase.threads.getThreadRecord(threadId)!!
threadRecord.unreadCount assertIs 0
}
@Test
fun handleSynchronizeReadWithEdits() {
val message1Timestamp = messageHelper.incomingText().timestamp
messageHelper.syncReadMessage(messageHelper.alice to message1Timestamp)
val editMessage1Timestamp1 = messageHelper.incomingEditText(message1Timestamp).timestamp
val editMessage1Timestamp2 = messageHelper.incomingEditText(editMessage1Timestamp1).timestamp
val message2Timestamp = messageHelper.incomingMedia().timestamp
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.alice)!!
var threadRecord = SignalDatabase.threads.getThreadRecord(threadId)!!
threadRecord.unreadCount assertIs 2
messageHelper.syncReadMessage(messageHelper.alice to message2Timestamp, messageHelper.alice to editMessage1Timestamp1, messageHelper.alice to editMessage1Timestamp2)
threadRecord = SignalDatabase.threads.getThreadRecord(threadId)!!
threadRecord.unreadCount assertIs 0
}
@Test
fun handleSynchronizeReadWithEditsInGroup() {
val message1Timestamp = messageHelper.incomingText(sender = messageHelper.alice, destination = messageHelper.group.recipientId).timestamp
messageHelper.syncReadMessage(messageHelper.alice to message1Timestamp)
val editMessage1Timestamp1 = messageHelper.incomingEditText(targetTimestamp = message1Timestamp, sender = messageHelper.alice, destination = messageHelper.group.recipientId).timestamp
val editMessage1Timestamp2 = messageHelper.incomingEditText(targetTimestamp = editMessage1Timestamp1, sender = messageHelper.alice, destination = messageHelper.group.recipientId).timestamp
val message2Timestamp = messageHelper.incomingMedia(sender = messageHelper.bob, destination = messageHelper.group.recipientId).timestamp
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.group.recipientId)!!
var threadRecord = SignalDatabase.threads.getThreadRecord(threadId)!!
threadRecord.unreadCount assertIs 2
messageHelper.syncReadMessage(messageHelper.bob to message2Timestamp, messageHelper.alice to editMessage1Timestamp1, messageHelper.alice to editMessage1Timestamp2)
threadRecord = SignalDatabase.threads.getThreadRecord(threadId)!!
threadRecord.unreadCount assertIs 0
}
}

View File

@@ -1,703 +0,0 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.messages
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.hamcrest.Matchers.greaterThan
import org.junit.After
import org.junit.Before
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.signal.core.util.logging.Log
import org.signal.core.util.update
import org.signal.core.util.withinTransaction
import org.thoughtcrime.securesms.attachments.Attachment
import org.thoughtcrime.securesms.attachments.DatabaseAttachment
import org.thoughtcrime.securesms.database.AttachmentTable
import org.thoughtcrime.securesms.database.CallTable
import org.thoughtcrime.securesms.database.MessageTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.databaseprotos.SessionSwitchoverEvent
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.testing.MessageContentFuzzer.DeleteForMeSync
import org.thoughtcrime.securesms.testing.SignalActivityRule
import org.thoughtcrime.securesms.testing.assert
import org.thoughtcrime.securesms.testing.assertIs
import org.thoughtcrime.securesms.testing.assertIsNot
import org.thoughtcrime.securesms.testing.assertIsNotNull
import org.thoughtcrime.securesms.testing.assertIsSize
import org.thoughtcrime.securesms.util.IdentityUtil
import java.util.UUID
@Suppress("ClassName")
@RunWith(AndroidJUnit4::class)
class SyncMessageProcessorTest_synchronizeDeleteForMe {
companion object {
private val TAG = "SyncDeleteForMeTest"
}
@get:Rule
val harness = SignalActivityRule(createGroup = true)
private lateinit var messageHelper: MessageHelper
@Before
fun setUp() {
messageHelper = MessageHelper(harness)
}
@After
fun tearDown() {
messageHelper.tearDown()
}
@Test
fun singleMessageDelete() {
// GIVEN
val message1Timestamp = messageHelper.incomingText().timestamp
messageHelper.incomingText()
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.alice)!!
var messageCount = SignalDatabase.messages.getMessageCountForThread(threadId)
messageCount assertIs 2
// WHEN
messageHelper.syncDeleteForMeMessage(
DeleteForMeSync(conversationId = messageHelper.alice, messageHelper.alice to message1Timestamp)
)
// THEN
messageCount = SignalDatabase.messages.getMessageCountForThread(threadId)
messageCount assertIs 1
}
@Test
fun singleOutgoingMessageDelete() {
// GIVEN
val message1Timestamp = messageHelper.outgoingText().timestamp
messageHelper.incomingText()
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.alice)!!
var messageCount = SignalDatabase.messages.getMessageCountForThread(threadId)
messageCount assertIs 2
// WHEN
messageHelper.syncDeleteForMeMessage(
DeleteForMeSync(conversationId = messageHelper.alice, harness.self.id to message1Timestamp)
)
// THEN
messageCount = SignalDatabase.messages.getMessageCountForThread(threadId)
messageCount assertIs 1
}
@Test
fun singleGroupMessageDelete() {
// GIVEN
val message1Timestamp = messageHelper.incomingText(sender = messageHelper.alice, destination = messageHelper.group.recipientId).timestamp
messageHelper.incomingText(sender = messageHelper.alice, destination = messageHelper.group.recipientId)
messageHelper.incomingText(sender = messageHelper.bob, destination = messageHelper.group.recipientId)
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.group.recipientId)!!
var messageCount = SignalDatabase.messages.getMessageCountForThread(threadId)
messageCount assertIs 3
// WHEN
messageHelper.syncDeleteForMeMessage(
DeleteForMeSync(conversationId = messageHelper.group.recipientId, messageHelper.alice to message1Timestamp)
)
// THEN
messageCount = SignalDatabase.messages.getMessageCountForThread(threadId)
messageCount assertIs 2
}
@Test
fun multipleGroupMessageDelete() {
// GIVEN
val message1Timestamp = messageHelper.incomingText(sender = messageHelper.alice, destination = messageHelper.group.recipientId).timestamp
messageHelper.incomingText(sender = messageHelper.alice, destination = messageHelper.group.recipientId)
val message3Timestamp = messageHelper.incomingText(sender = messageHelper.bob, destination = messageHelper.group.recipientId).timestamp
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.group.recipientId)!!
var messageCount = SignalDatabase.messages.getMessageCountForThread(threadId)
messageCount assertIs 3
// WHEN
messageHelper.syncDeleteForMeMessage(
DeleteForMeSync(conversationId = messageHelper.group.recipientId, messageHelper.alice to message1Timestamp, messageHelper.bob to message3Timestamp)
)
// THEN
messageCount = SignalDatabase.messages.getMessageCountForThread(threadId)
messageCount assertIs 1
}
@Test
fun allMessagesDelete() {
// GIVEN
val message1Timestamp = messageHelper.incomingText().timestamp
val message2Timestamp = messageHelper.incomingText().timestamp
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.alice)!!
var messageCount = SignalDatabase.messages.getMessageCountForThread(threadId)
messageCount assertIs 2
// WHEN
messageHelper.syncDeleteForMeMessage(
DeleteForMeSync(conversationId = messageHelper.alice, messageHelper.alice to message1Timestamp, messageHelper.alice to message2Timestamp)
)
// THEN
messageCount = SignalDatabase.messages.getMessageCountForThread(threadId)
messageCount assertIs 0
val threadRecord = SignalDatabase.threads.getThreadRecord(threadId)
threadRecord assertIs null
}
@Test
fun earlyMessagesDelete() {
// GIVEN
messageHelper.incomingText().timestamp
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.alice)!!
var messageCount = SignalDatabase.messages.getMessageCountForThread(threadId)
messageCount assertIs 1
// WHEN
val nextTextMessageTimestamp = messageHelper.nextStartTime(2)
messageHelper.syncDeleteForMeMessage(
DeleteForMeSync(conversationId = messageHelper.alice, messageHelper.alice to nextTextMessageTimestamp)
)
messageHelper.incomingText()
// THEN
messageCount = SignalDatabase.messages.getMessageCountForThread(threadId)
messageCount assertIs 1
}
@Test
fun multipleConversationMessagesDelete() {
// GIVEN
messageHelper.incomingText(sender = messageHelper.alice)
val aliceMessage2 = messageHelper.incomingText(sender = messageHelper.alice).timestamp
messageHelper.incomingText(sender = messageHelper.bob)
val bobMessage2 = messageHelper.incomingText(sender = messageHelper.bob).timestamp
val aliceThreadId = SignalDatabase.threads.getThreadIdFor(messageHelper.alice)!!
var aliceMessageCount = SignalDatabase.messages.getMessageCountForThread(aliceThreadId)
aliceMessageCount assertIs 2
val bobThreadId = SignalDatabase.threads.getThreadIdFor(messageHelper.bob)!!
var bobMessageCount = SignalDatabase.messages.getMessageCountForThread(bobThreadId)
bobMessageCount assertIs 2
// WHEN
messageHelper.syncDeleteForMeMessage(
DeleteForMeSync(conversationId = messageHelper.alice, messageHelper.alice to aliceMessage2),
DeleteForMeSync(conversationId = messageHelper.bob, messageHelper.bob to bobMessage2)
)
// THEN
aliceMessageCount = SignalDatabase.messages.getMessageCountForThread(aliceThreadId)
aliceMessageCount assertIs 1
bobMessageCount = SignalDatabase.messages.getMessageCountForThread(bobThreadId)
bobMessageCount assertIs 1
}
@Test
fun singleConversationDelete() {
// GIVEN
val messages = mutableListOf<MessageTable.SyncMessageId>()
for (i in 0 until 10) {
messages += MessageTable.SyncMessageId(messageHelper.alice, messageHelper.incomingText().timestamp)
messages += MessageTable.SyncMessageId(harness.self.id, messageHelper.outgoingText().timestamp)
}
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.alice)!!
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 20
// WHEN
messageHelper.syncDeleteForMeConversation(
DeleteForMeSync(
conversationId = messageHelper.alice,
messages = messages.takeLast(5).map { it.recipientId to it.timetamp },
isFullDelete = true
)
)
// THEN
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 0
SignalDatabase.threads.getThreadRecord(threadId) assertIs null
}
@Test
fun singleConversationNoRecentsFoundDelete() {
// GIVEN
val messages = mutableListOf<MessageTable.SyncMessageId>()
for (i in 0 until 10) {
messages += MessageTable.SyncMessageId(messageHelper.alice, messageHelper.incomingText().timestamp)
messages += MessageTable.SyncMessageId(harness.self.id, messageHelper.outgoingText().timestamp)
}
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.alice)!!
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 20
// WHEN
val randomFutureMessages = (1..5).map {
messageHelper.alice to messageHelper.nextStartTime(it)
}
messageHelper.syncDeleteForMeConversation(
DeleteForMeSync(conversationId = messageHelper.alice, randomFutureMessages, isFullDelete = true)
)
// THEN
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 20
SignalDatabase.threads.getThreadRecord(threadId).assertIsNotNull()
harness.inMemoryLogger.flush()
harness.inMemoryLogger.entries().filter { it.message?.contains("Unable to find most recent received at timestamp") == true }.size assertIs 1
}
@Test
fun singleConversationNoRecentsFoundNonExpiringRecentsFoundDelete() {
// GIVEN
val messages = mutableListOf<MessageTable.SyncMessageId>()
for (i in 0 until 10) {
messages += MessageTable.SyncMessageId(messageHelper.alice, messageHelper.incomingText().timestamp)
messages += MessageTable.SyncMessageId(harness.self.id, messageHelper.outgoingText().timestamp)
}
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.alice)!!
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 20
// WHEN
val nonExpiringMessages = messages.takeLast(5).map { it.recipientId to it.timetamp }
val randomFutureMessages = (1..5).map {
messageHelper.alice to messageHelper.nextStartTime(it)
}
messageHelper.syncDeleteForMeConversation(
DeleteForMeSync(conversationId = messageHelper.alice, randomFutureMessages, nonExpiringMessages, true)
)
// THEN
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 0
SignalDatabase.threads.getThreadRecord(threadId) assertIs null
harness.inMemoryLogger.flush()
harness.inMemoryLogger.entries().filter { it.message?.contains("Using backup non-expiring messages") == true }.size assertIs 1
}
@Test
fun localOnlyRemainingAfterConversationDeleteWithFullDelete() {
// GIVEN
val messages = mutableListOf<MessageTable.SyncMessageId>()
Log.v(TAG, "Adding normal messages")
for (i in 0 until 10) {
messages += MessageTable.SyncMessageId(messageHelper.alice, messageHelper.incomingText().timestamp)
messages += MessageTable.SyncMessageId(harness.self.id, messageHelper.outgoingText().timestamp)
}
val alice = Recipient.resolved(messageHelper.alice)
Log.v(TAG, "Adding identity message")
IdentityUtil.markIdentityVerified(harness.context, alice, true, true)
Log.v(TAG, "Adding profile message")
SignalDatabase.messages.insertProfileNameChangeMessages(alice, "new name", "previous name")
Log.v(TAG, "Adding call message")
SignalDatabase.calls.insertOneToOneCall(1, System.currentTimeMillis(), alice.id, CallTable.Type.AUDIO_CALL, CallTable.Direction.OUTGOING, CallTable.Event.ACCEPTED)
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.alice)!!
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 23
// WHEN
Log.v(TAG, "Processing sync message")
messageHelper.syncDeleteForMeConversation(
DeleteForMeSync(
conversationId = messageHelper.alice,
messages = messages.takeLast(5).map { it.recipientId to it.timetamp },
isFullDelete = true
)
)
// THEN
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 0
SignalDatabase.threads.getThreadRecord(threadId) assertIs null
}
@Test
fun localOnlyRemainingAfterConversationDeleteWithoutFullDelete() {
// GIVEN
val messages = mutableListOf<MessageTable.SyncMessageId>()
for (i in 0 until 10) {
messages += MessageTable.SyncMessageId(messageHelper.alice, messageHelper.incomingText().timestamp)
messages += MessageTable.SyncMessageId(harness.self.id, messageHelper.outgoingText().timestamp)
}
val alice = Recipient.resolved(messageHelper.alice)
IdentityUtil.markIdentityVerified(harness.context, alice, true, true)
SignalDatabase.messages.insertProfileNameChangeMessages(alice, "new name", "previous name")
SignalDatabase.calls.insertOneToOneCall(1, System.currentTimeMillis(), alice.id, CallTable.Type.AUDIO_CALL, CallTable.Direction.OUTGOING, CallTable.Event.ACCEPTED)
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.alice)!!
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 23
// WHEN
messageHelper.syncDeleteForMeConversation(
DeleteForMeSync(
conversationId = messageHelper.alice,
messages = messages.takeLast(5).map { it.recipientId to it.timetamp },
isFullDelete = false
)
)
// THEN
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 3
SignalDatabase.threads.getThreadRecord(threadId).assertIsNotNull()
}
@Test
fun groupConversationDelete() {
// GIVEN
val messages = mutableListOf<MessageTable.SyncMessageId>()
for (i in 0 until 50) {
messages += when (i % 3) {
1 -> MessageTable.SyncMessageId(messageHelper.alice, messageHelper.incomingText(sender = messageHelper.alice, destination = messageHelper.group.recipientId).timestamp)
2 -> MessageTable.SyncMessageId(messageHelper.bob, messageHelper.incomingText(sender = messageHelper.bob, destination = messageHelper.group.recipientId).timestamp)
else -> MessageTable.SyncMessageId(harness.self.id, messageHelper.outgoingText(messageHelper.group.recipientId).timestamp)
}
}
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.group.recipientId)!!
// WHEN
messageHelper.syncDeleteForMeConversation(
DeleteForMeSync(
conversationId = messageHelper.group.recipientId,
messages = messages.takeLast(5).map { it.recipientId to it.timetamp },
isFullDelete = true
)
)
// THEN
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 0
SignalDatabase.threads.getThreadRecord(threadId) assertIs null
}
@Test
fun multipleConversationDelete() {
// GIVEN
val allMessages = mapOf<RecipientId, MutableList<MessageTable.SyncMessageId>>(
messageHelper.alice to mutableListOf(),
messageHelper.bob to mutableListOf()
)
allMessages.forEach { (conversation, messages) ->
for (i in 0 until 10) {
messages += MessageTable.SyncMessageId(conversation, messageHelper.incomingText(sender = conversation).timestamp)
messages += MessageTable.SyncMessageId(harness.self.id, messageHelper.outgoingText(conversationId = conversation).timestamp)
}
}
val threadIds = allMessages.keys.map { SignalDatabase.threads.getThreadIdFor(it)!! }
threadIds.forEach { SignalDatabase.messages.getMessageCountForThread(it) assertIs 20 }
// WHEN
messageHelper.syncDeleteForMeConversation(
DeleteForMeSync(conversationId = messageHelper.alice, allMessages[messageHelper.alice]!!.takeLast(5).map { it.recipientId to it.timetamp }, isFullDelete = true),
DeleteForMeSync(conversationId = messageHelper.bob, allMessages[messageHelper.bob]!!.takeLast(5).map { it.recipientId to it.timetamp }, isFullDelete = true)
)
// THEN
threadIds.forEach {
SignalDatabase.messages.getMessageCountForThread(it) assertIs 0
SignalDatabase.threads.getThreadRecord(it) assertIs null
}
}
@Test
fun singleLocalOnlyConversation() {
// GIVEN
val alice = Recipient.resolved(messageHelper.alice)
// Insert placeholder message to prevent early thread update deletes
val oneToOnePlaceHolderMessage = messageHelper.outgoingText().messageId
val aliceThreadId = SignalDatabase.threads.getOrCreateThreadIdFor(messageHelper.alice, isGroup = false)
IdentityUtil.markIdentityVerified(harness.context, alice, true, false)
SignalDatabase.calls.insertOneToOneCall(1, System.currentTimeMillis(), alice.id, CallTable.Type.AUDIO_CALL, CallTable.Direction.OUTGOING, CallTable.Event.ACCEPTED)
SignalDatabase.messages.insertProfileNameChangeMessages(alice, "new name", "previous name")
SignalDatabase.messages.markAsSentFailed(messageHelper.outgoingText().messageId)
// Cleanup and confirm setup
SignalDatabase.messages.deleteMessage(messageId = oneToOnePlaceHolderMessage, threadId = aliceThreadId, notify = false, updateThread = false)
SignalDatabase.messages.getMessageCountForThread(aliceThreadId) assert greaterThan(0)
// WHEN
messageHelper.syncDeleteForMeLocalOnlyConversation(messageHelper.alice)
// THEN
SignalDatabase.messages.getMessageCountForThread(aliceThreadId) assertIs 0
SignalDatabase.threads.getThreadRecord(aliceThreadId) assertIs null
}
@Ignore("counts are consistent for some reason")
@Test
fun multipleLocalOnlyConversation() {
// GIVEN
val alice = Recipient.resolved(messageHelper.alice)
// Insert placeholder messages in group and alice thread to prevent early thread update deletes
val groupPlaceholderMessage = messageHelper.outgoingText(conversationId = messageHelper.group.recipientId).messageId
val oneToOnePlaceHolderMessage = messageHelper.outgoingText().messageId
val aliceThreadId = SignalDatabase.threads.getOrCreateThreadIdFor(messageHelper.alice, isGroup = false)
val groupThreadId = SignalDatabase.threads.getOrCreateThreadIdFor(messageHelper.group.recipientId, isGroup = true)
// Identity changes
IdentityUtil.markIdentityVerified(harness.context, alice, true, true)
IdentityUtil.markIdentityVerified(harness.context, alice, false, true)
IdentityUtil.markIdentityVerified(harness.context, alice, true, false)
IdentityUtil.markIdentityVerified(harness.context, alice, false, false)
IdentityUtil.markIdentityUpdate(harness.context, alice.id)
// Calls
SignalDatabase.calls.insertOneToOneCall(1, System.currentTimeMillis(), alice.id, CallTable.Type.AUDIO_CALL, CallTable.Direction.OUTGOING, CallTable.Event.ACCEPTED)
SignalDatabase.calls.insertOneToOneCall(2, System.currentTimeMillis(), alice.id, CallTable.Type.VIDEO_CALL, CallTable.Direction.INCOMING, CallTable.Event.MISSED)
SignalDatabase.calls.insertOneToOneCall(3, System.currentTimeMillis(), alice.id, CallTable.Type.AUDIO_CALL, CallTable.Direction.INCOMING, CallTable.Event.MISSED_NOTIFICATION_PROFILE)
SignalDatabase.calls.insertAcceptedGroupCall(4, messageHelper.group.recipientId, CallTable.Direction.INCOMING, System.currentTimeMillis())
SignalDatabase.calls.insertDeclinedGroupCall(5, messageHelper.group.recipientId, System.currentTimeMillis())
// Detected changes
SignalDatabase.messages.insertProfileNameChangeMessages(alice, "new name", "previous name")
SignalDatabase.messages.insertLearnedProfileNameChangeMessage(alice, null, "username.42")
SignalDatabase.messages.insertNumberChangeMessages(alice.id)
SignalDatabase.messages.insertSmsExportMessage(alice.id, SignalDatabase.threads.getThreadIdFor(messageHelper.alice)!!)
SignalDatabase.messages.insertSessionSwitchoverEvent(alice.id, aliceThreadId, SessionSwitchoverEvent())
// Sent failed
SignalDatabase.messages.markAsSending(messageHelper.outgoingText().messageId)
SignalDatabase.messages.markAsSentFailed(messageHelper.outgoingText().messageId)
messageHelper.outgoingText().let {
SignalDatabase.messages.markAsSending(it.messageId)
SignalDatabase.messages.markAsRateLimited(it.messageId)
}
// Group change
messageHelper.outgoingGroupChange()
// Cleanup and confirm setup
SignalDatabase.messages.deleteMessage(messageId = oneToOnePlaceHolderMessage, threadId = aliceThreadId, notify = false, updateThread = false)
SignalDatabase.messages.deleteMessage(messageId = groupPlaceholderMessage, threadId = aliceThreadId, notify = false, updateThread = false)
SignalDatabase.rawDatabase.withinTransaction {
SignalDatabase.messages.getMessageCountForThread(aliceThreadId) assertIs 16
SignalDatabase.messages.getMessageCountForThread(groupThreadId) assertIs 10
}
// WHEN
messageHelper.syncDeleteForMeLocalOnlyConversation(messageHelper.alice, messageHelper.group.recipientId)
// THEN
SignalDatabase.messages.getMessageCountForThread(aliceThreadId) assertIs 0
SignalDatabase.threads.getThreadRecord(aliceThreadId) assertIs null
SignalDatabase.messages.getMessageCountForThread(groupThreadId) assertIs 0
SignalDatabase.threads.getThreadRecord(groupThreadId) assertIs null
}
@Test
fun singleLocalOnlyConversationHasAddressable() {
// GIVEN
val messages = mutableListOf<MessageTable.SyncMessageId>()
for (i in 0 until 10) {
messages += MessageTable.SyncMessageId(messageHelper.alice, messageHelper.incomingText().timestamp)
messages += MessageTable.SyncMessageId(harness.self.id, messageHelper.outgoingText().timestamp)
}
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.alice)!!
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 20
// WHEN
messageHelper.syncDeleteForMeLocalOnlyConversation(messageHelper.alice)
// THEN
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 20
SignalDatabase.threads.getThreadRecord(threadId).assertIsNotNull()
harness.inMemoryLogger.flush()
harness.inMemoryLogger.entries().filter { it.message?.contains("Thread is not local only") == true }.size assertIs 1
}
@Test
fun singleAttachmentDeletes() {
// GIVEN
val message1 = messageHelper.outgoingText { message ->
message.copy(
attachments = listOf(
messageHelper.outgoingAttachment(byteArrayOf(1, 2, 3)),
messageHelper.outgoingAttachment(byteArrayOf(2, 3, 4), null),
messageHelper.outgoingAttachment(byteArrayOf(5, 6, 7), null),
messageHelper.outgoingAttachment(byteArrayOf(10, 11, 12))
)
)
}
var attachments = SignalDatabase.attachments.getAttachmentsForMessage(message1.messageId)
attachments assertIsSize 4
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.alice)!!
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 1
// Has all three
SignalDatabase.attachments.finalizeAttachmentAfterUpload(
id = attachments[0].attachmentId,
attachment = attachments[0].copy(digest = byteArrayOf(attachments[0].attachmentId.id.toByte())),
uploadTimestamp = message1.timestamp + 1
)
// Missing uuid and digest
SignalDatabase.attachments.finalizeAttachmentAfterUpload(
id = attachments[1].attachmentId,
attachment = attachments[1],
uploadTimestamp = message1.timestamp + 1
)
// Missing uuid and plain text
SignalDatabase.attachments.finalizeAttachmentAfterUpload(
id = attachments[2].attachmentId,
attachment = attachments[2].copy(digest = byteArrayOf(attachments[2].attachmentId.id.toByte())),
uploadTimestamp = message1.timestamp + 1
)
SignalDatabase.rawDatabase.update(AttachmentTable.TABLE_NAME).values(AttachmentTable.DATA_HASH_END to null).where("${AttachmentTable.ID} = ?", attachments[2].attachmentId).run()
// Different has all three
SignalDatabase.attachments.finalizeAttachmentAfterUpload(
id = attachments[3].attachmentId,
attachment = attachments[3].copy(digest = byteArrayOf(attachments[3].attachmentId.id.toByte())),
uploadTimestamp = message1.timestamp + 1
)
attachments = SignalDatabase.attachments.getAttachmentsForMessage(message1.messageId)
// WHEN
messageHelper.syncDeleteForMeAttachment(
conversationId = messageHelper.alice,
message = message1.author to message1.timestamp,
attachments[0].uuid,
attachments[0].remoteDigest,
attachments[0].dataHash
)
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 1
var updatedAttachments = SignalDatabase.attachments.getAttachmentsForMessage(message1.messageId)
updatedAttachments assertIsSize 3
updatedAttachments.forEach { it.attachmentId assertIsNot attachments[0].attachmentId }
messageHelper.syncDeleteForMeAttachment(
conversationId = messageHelper.alice,
message = message1.author to message1.timestamp,
attachments[1].uuid,
attachments[1].remoteDigest,
attachments[1].dataHash
)
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 1
updatedAttachments = SignalDatabase.attachments.getAttachmentsForMessage(message1.messageId)
updatedAttachments assertIsSize 2
updatedAttachments.forEach { it.attachmentId assertIsNot attachments[1].attachmentId }
messageHelper.syncDeleteForMeAttachment(
conversationId = messageHelper.alice,
message = message1.author to message1.timestamp,
attachments[2].uuid,
attachments[2].remoteDigest,
attachments[2].dataHash
)
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 1
updatedAttachments = SignalDatabase.attachments.getAttachmentsForMessage(message1.messageId)
updatedAttachments assertIsSize 1
updatedAttachments.forEach { it.attachmentId assertIsNot attachments[2].attachmentId }
messageHelper.syncDeleteForMeAttachment(
conversationId = messageHelper.alice,
message = message1.author to message1.timestamp,
attachments[3].uuid,
attachments[3].remoteDigest,
attachments[3].dataHash
)
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 0
updatedAttachments = SignalDatabase.attachments.getAttachmentsForMessage(message1.messageId)
updatedAttachments assertIsSize 0
SignalDatabase.threads.getThreadRecord(threadId) assertIs null
}
private fun DatabaseAttachment.copy(
uuid: UUID? = this.uuid,
digest: ByteArray? = this.remoteDigest
): Attachment {
return DatabaseAttachment(
attachmentId = this.attachmentId,
mmsId = this.mmsId,
hasData = this.hasData,
hasThumbnail = false,
hasArchiveThumbnail = false,
contentType = this.contentType,
transferProgress = this.transferState,
size = this.size,
fileName = this.fileName,
cdn = this.cdn,
location = this.remoteLocation,
key = this.remoteKey,
digest = digest,
incrementalDigest = this.incrementalDigest,
incrementalMacChunkSize = this.incrementalMacChunkSize,
fastPreflightId = this.fastPreflightId,
voiceNote = this.voiceNote,
borderless = this.borderless,
videoGif = this.videoGif,
width = this.width,
height = this.height,
quote = this.quote,
caption = this.caption,
stickerLocator = this.stickerLocator,
blurHash = this.blurHash,
audioHash = this.audioHash,
transformProperties = this.transformProperties,
displayOrder = this.displayOrder,
uploadTimestamp = this.uploadTimestamp,
dataHash = this.dataHash,
archiveCdn = this.archiveCdn,
archiveThumbnailCdn = this.archiveThumbnailCdn,
archiveMediaName = this.archiveMediaName,
archiveMediaId = this.archiveMediaId,
thumbnailRestoreState = this.thumbnailRestoreState,
uuid = uuid
)
}
}

View File

@@ -1,55 +0,0 @@
package org.thoughtcrime.securesms.migrations
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.signal.core.util.count
import org.signal.core.util.readToSingleInt
import org.signal.donations.PaymentSourceType
import org.thoughtcrime.securesms.database.InAppPaymentSubscriberTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.testing.assertIs
import org.thoughtcrime.securesms.testing.assertIsNotNull
import org.whispersystems.signalservice.api.subscriptions.SubscriberId
import java.util.Currency
@RunWith(AndroidJUnit4::class)
class SubscriberIdMigrationJobTest {
private val testSubject = SubscriberIdMigrationJob()
@Test
fun givenNoSubscriber_whenIRunSubscriberIdMigrationJob_thenIExpectNoDatabaseEntries() {
testSubject.run()
val actual = SignalDatabase.inAppPaymentSubscribers.readableDatabase.count()
.from(InAppPaymentSubscriberTable.TABLE_NAME)
.run()
.readToSingleInt()
actual assertIs 0
}
@Test
fun givenUSDSubscriber_whenIRunSubscriberIdMigrationJob_thenIExpectASingleEntry() {
val subscriberId = SubscriberId.generate()
SignalStore.inAppPayments.setSubscriberCurrency(Currency.getInstance("USD"), InAppPaymentSubscriberRecord.Type.DONATION)
SignalStore.inAppPayments.setSubscriber("USD", subscriberId)
SignalStore.inAppPayments.setSubscriptionPaymentSourceType(PaymentSourceType.PayPal)
SignalStore.inAppPayments.shouldCancelSubscriptionBeforeNextSubscribeAttempt = true
testSubject.run()
val actual = SignalDatabase.inAppPaymentSubscribers.getByCurrencyCode("USD", InAppPaymentSubscriberRecord.Type.DONATION)
actual.assertIsNotNull()
actual!!.subscriberId.bytes assertIs subscriberId.bytes
actual.paymentMethodType assertIs InAppPaymentData.PaymentMethodType.PAYPAL
actual.requiresCancel assertIs true
actual.currency assertIs Currency.getInstance("USD")
actual.type assertIs InAppPaymentSubscriberRecord.Type.DONATION
}
}

View File

@@ -57,6 +57,23 @@ class UsernameEditFragmentTest {
InstrumentationApplicationDependencyProvider.clearHandlers()
}
@Test
fun testUsernameCreationInRegistration() {
val scenario = createScenario(UsernameEditMode.REGISTRATION)
scenario.moveToState(Lifecycle.State.RESUMED)
onView(withId(R.id.toolbar)).check { view, noViewFoundException ->
noViewFoundException.assertIsNull()
val toolbar = view as Toolbar
toolbar.navigationIcon.assertIsNull()
}
onView(withText(R.string.UsernameEditFragment__add_a_username)).check(matches(isDisplayed()))
onView(withContentDescription(R.string.load_more_header__loading)).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE)))
}
@Ignore("Flakey espresso test.")
@Test
fun testUsernameCreationOutsideOfRegistration() {
@@ -91,7 +108,7 @@ class UsernameEditFragmentTest {
}
)
val scenario = createScenario(UsernameEditMode.NORMAL)
val scenario = createScenario(UsernameEditMode.REGISTRATION)
scenario.moveToState(Lifecycle.State.RESUMED)
onView(withId(R.id.username_text)).perform(typeText(nickname))

View File

@@ -90,10 +90,10 @@ class SafetyNumberBottomSheetRepositoryTest {
subjectUnderTest.removeFromStories(toRemove, listOf(destinationKey)).subscribe()
testSubscriber.request(1)
testScheduler.triggerActions()
testSubscriber.awaitCount(2)
testSubscriber.awaitCount(3)
// THEN
testSubscriber.assertValueAt(1) { map ->
testSubscriber.assertValueAt(2) { map ->
assertMatch(
map,
mapOf(
@@ -116,10 +116,10 @@ class SafetyNumberBottomSheetRepositoryTest {
subjectUnderTest.removeAllFromStory(distributionListMembers, distributionList).subscribe()
testSubscriber.request(1)
testScheduler.triggerActions()
testSubscriber.awaitCount(2)
testSubscriber.awaitCount(3)
// THEN
testSubscriber.assertValueAt(1) { map ->
testSubscriber.assertValueAt(2) { map ->
assertMatch(map, mapOf())
}
}

View File

@@ -12,6 +12,8 @@ import org.thoughtcrime.securesms.database.RecipientTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.util.FeatureFlags
import org.thoughtcrime.securesms.util.FeatureFlagsAccessor
import org.whispersystems.signalservice.api.push.ServiceId.ACI
import org.whispersystems.signalservice.api.push.ServiceId.PNI
import org.whispersystems.signalservice.api.storage.SignalContactRecord
@@ -24,9 +26,10 @@ class ContactRecordProcessorTest {
@Before
fun setup() {
SignalStore.account.setE164(E164_SELF)
SignalStore.account.setAci(ACI_SELF)
SignalStore.account.setPni(PNI_SELF)
SignalStore.account().setE164(E164_SELF)
SignalStore.account().setAci(ACI_SELF)
SignalStore.account().setPni(PNI_SELF)
FeatureFlagsAccessor.forceValue(FeatureFlags.PHONE_NUMBER_PRIVACY, true)
}
@Test

View File

@@ -3,7 +3,8 @@ package org.thoughtcrime.securesms.testing
import org.signal.core.util.logging.Log
import org.signal.libsignal.protocol.ecc.ECKeyPair
import org.signal.libsignal.zkgroup.profiles.ProfileKey
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.messages.protocol.BufferedProtocolStore
import org.thoughtcrime.securesms.recipients.Recipient
@@ -29,14 +30,14 @@ class AliceClient(val serviceId: ServiceId, val e164: String, val trustRoot: ECK
uuid = serviceId.rawUuid,
e164 = e164,
deviceId = 1,
identityKey = SignalStore.account.aciIdentityKey.publicKey.publicKey,
identityKey = SignalStore.account().aciIdentityKey.publicKey.publicKey,
expires = 31337
)
fun process(envelope: Envelope, serverDeliveredTimestamp: Long) {
val start = System.currentTimeMillis()
val bufferedStore = BufferedProtocolStore.create()
AppDependencies.incomingMessageObserver
ApplicationDependencies.getIncomingMessageObserver()
.processEnvelope(bufferedStore, envelope, serverDeliveredTimestamp)
?.mapNotNull { it.run() }
?.forEach { it.enqueue() }
@@ -47,9 +48,9 @@ class AliceClient(val serviceId: ServiceId, val e164: String, val trustRoot: ECK
}
fun encrypt(now: Long, destination: Recipient): Envelope {
return AppDependencies.signalServiceMessageSender.getEncryptedMessage(
return ApplicationDependencies.getSignalServiceMessageSender().getEncryptedMessage(
SignalServiceAddress(destination.requireServiceId(), destination.requireE164()),
FakeClientHelpers.getSealedSenderAccess(ProfileKey(destination.profileKey), aliceSenderCertificate),
FakeClientHelpers.getTargetUnidentifiedAccess(ProfileKeyUtil.getSelfProfileKey(), ProfileKey(destination.profileKey), aliceSenderCertificate),
1,
FakeClientHelpers.encryptedTextMessage(now),
false

View File

@@ -17,7 +17,7 @@ import org.signal.libsignal.protocol.state.SignedPreKeyRecord
import org.signal.libsignal.protocol.util.KeyHelper
import org.signal.libsignal.zkgroup.profiles.ProfileKey
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil
import org.thoughtcrime.securesms.crypto.SealedSenderAccessUtil
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil
import org.thoughtcrime.securesms.database.OneTimePreKeyTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.SignedPreKeyTable
@@ -25,13 +25,14 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.testing.FakeClientHelpers.toEnvelope
import org.whispersystems.signalservice.api.SignalServiceAccountDataStore
import org.whispersystems.signalservice.api.SignalSessionLock
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.ServiceId
import org.whispersystems.signalservice.api.push.SignalServiceAddress
import org.whispersystems.signalservice.internal.push.Envelope
import java.util.Optional
import java.util.UUID
import java.util.concurrent.locks.ReentrantLock
@@ -74,12 +75,12 @@ class BobClient(val serviceId: ServiceId, val e164: String, val identityKeyPair:
}
fun decrypt(envelope: Envelope, serverDeliveredTimestamp: Long) {
val cipher = SignalServiceCipher(serviceAddress, 1, aciStore, sessionLock, SealedSenderAccessUtil.getCertificateValidator())
val cipher = SignalServiceCipher(serviceAddress, 1, aciStore, sessionLock, UnidentifiedAccessUtil.getCertificateValidator())
cipher.decrypt(envelope, serverDeliveredTimestamp)
}
private fun getAliceServiceId(): ServiceId {
return SignalStore.account.requireAci()
return SignalStore.account().requireAci()
}
private fun getAlicePreKeyBundle(): PreKeyBundle {
@@ -102,7 +103,7 @@ class BobClient(val serviceId: ServiceId, val e164: String, val identityKeyPair:
val selfSignedPreKeyRecord = SignalDatabase.signedPreKeys.get(getAliceServiceId(), selfSignedPreKeyId)!!
return PreKeyBundle(
SignalStore.account.registrationId,
SignalStore.account().registrationId,
1,
selfPreKeyId,
selfPreKeyRecord.keyPair.publicKey,
@@ -114,19 +115,19 @@ class BobClient(val serviceId: ServiceId, val e164: String, val identityKeyPair:
}
private fun getAliceProtocolAddress(): SignalProtocolAddress {
return SignalProtocolAddress(SignalStore.account.requireAci().toString(), 1)
return SignalProtocolAddress(SignalStore.account().requireAci().toString(), 1)
}
private fun getAlicePublicKey(): IdentityKey {
return SignalStore.account.aciIdentityKey.publicKey
return SignalStore.account().aciIdentityKey.publicKey
}
private fun getAliceProfileKey(): ProfileKey {
return ProfileKeyUtil.getSelfProfileKey()
}
private fun getAliceUnidentifiedAccess(): SealedSenderAccess? {
return FakeClientHelpers.getSealedSenderAccess(getAliceProfileKey(), senderCertificate)
private fun getAliceUnidentifiedAccess(): Optional<UnidentifiedAccess> {
return FakeClientHelpers.getTargetUnidentifiedAccess(profileKey, getAliceProfileKey(), senderCertificate)
}
private class BobSignalServiceAccountDataStore(private val registrationId: Int, private val identityKeyPair: IdentityKeyPair) : SignalServiceAccountDataStore {
@@ -138,12 +139,10 @@ class BobClient(val serviceId: ServiceId, val e164: String, val identityKeyPair:
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?): Boolean = false
override fun storeSession(address: SignalProtocolAddress?, record: SessionRecord?) {
aliceSessionRecord = record
}
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 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()

View File

@@ -14,8 +14,8 @@ import org.signal.libsignal.zkgroup.profiles.ProfileKey
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.api.crypto.SealedSenderAccess
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair
import org.whispersystems.signalservice.api.push.ServiceId
import org.whispersystems.signalservice.internal.push.Content
import org.whispersystems.signalservice.internal.push.DataMessage
@@ -46,10 +46,11 @@ object FakeClientHelpers {
}
}
fun getSealedSenderAccess(theirProfileKey: ProfileKey, senderCertificate: SenderCertificate): SealedSenderAccess? {
val themUnidentifiedAccessKey = UnidentifiedAccess(UnidentifiedAccess.deriveAccessKeyFrom(theirProfileKey), senderCertificate.serialized, false)
fun getTargetUnidentifiedAccess(myProfileKey: ProfileKey, theirProfileKey: ProfileKey, senderCertificate: SenderCertificate): Optional<UnidentifiedAccess> {
val selfUnidentifiedAccessKey = UnidentifiedAccess.deriveAccessKeyFrom(myProfileKey)
val themUnidentifiedAccessKey = UnidentifiedAccess.deriveAccessKeyFrom(theirProfileKey)
return SealedSenderAccess.forIndividual(themUnidentifiedAccessKey)
return UnidentifiedAccessPair(UnidentifiedAccess(selfUnidentifiedAccessKey, senderCertificate.serialized, false), UnidentifiedAccess(themUnidentifiedAccessKey, senderCertificate.serialized, false)).targetUnidentifiedAccess
}
fun encryptedTextMessage(now: Long, message: String = "Test body message"): EnvelopeContent {

View File

@@ -1,6 +1,5 @@
package org.thoughtcrime.securesms.testing
import okio.ByteString.Companion.toByteString
import org.signal.libsignal.zkgroup.groups.GroupMasterKey
import org.signal.storageservice.protos.groups.Member
import org.signal.storageservice.protos.groups.local.DecryptedGroup
@@ -10,7 +9,6 @@ import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.whispersystems.signalservice.api.push.ServiceId.ACI
import org.whispersystems.signalservice.internal.push.GroupContextV2
import kotlin.random.Random
/**
@@ -33,7 +31,7 @@ object GroupTestingUtils {
.title(MessageContentFuzzer.string())
.build()
val groupId = SignalDatabase.groups.create(groupMasterKey, decryptedGroupState, null)!!
val groupId = SignalDatabase.groups.create(groupMasterKey, decryptedGroupState)!!
val groupRecipientId = SignalDatabase.recipients.getOrInsertFromGroupId(groupId)
SignalDatabase.recipients.setProfileSharing(groupRecipientId, true)
@@ -48,8 +46,5 @@ object GroupTestingUtils {
return member(aci = requireAci())
}
data class TestGroupInfo(val groupId: GroupId.V2, val masterKey: GroupMasterKey, val recipientId: RecipientId) {
val groupV2Context: GroupContextV2
get() = GroupContextV2(masterKey = masterKey.serialize().toByteString(), revision = 0)
}
data class TestGroupInfo(val groupId: GroupId.V2, val masterKey: GroupMasterKey, val recipientId: RecipientId)
}

View File

@@ -2,20 +2,16 @@ package org.thoughtcrime.securesms.testing
import okio.ByteString
import okio.ByteString.Companion.toByteString
import org.signal.core.util.Base64
import org.thoughtcrime.securesms.database.AttachmentTable
import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.messages.SignalServiceProtoUtil.buildWith
import org.thoughtcrime.securesms.messages.TestMessage
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.whispersystems.signalservice.api.crypto.EnvelopeMetadata
import org.whispersystems.signalservice.api.util.UuidUtil
import org.whispersystems.signalservice.internal.push.AttachmentPointer
import org.whispersystems.signalservice.internal.push.BodyRange
import org.whispersystems.signalservice.internal.push.Content
import org.whispersystems.signalservice.internal.push.DataMessage
import org.whispersystems.signalservice.internal.push.EditMessage
import org.whispersystems.signalservice.internal.push.Envelope
import org.whispersystems.signalservice.internal.push.GroupContextV2
import org.whispersystems.signalservice.internal.push.SyncMessage
@@ -37,22 +33,22 @@ object MessageContentFuzzer {
/**
* Create an [Envelope].
*/
fun envelope(timestamp: Long, serverGuid: UUID = UUID.randomUUID()): Envelope {
fun envelope(timestamp: Long): Envelope {
return Envelope.Builder()
.timestamp(timestamp)
.serverTimestamp(timestamp + 5)
.serverGuid(serverGuid.toString())
.serverGuid(UUID.randomUUID().toString())
.build()
}
/**
* Create metadata to match an [Envelope].
*/
fun envelopeMetadata(source: RecipientId, destination: RecipientId, sourceDeviceId: Int = 1, groupId: GroupId.V2? = null): EnvelopeMetadata {
fun envelopeMetadata(source: RecipientId, destination: RecipientId, groupId: GroupId.V2? = null): EnvelopeMetadata {
return EnvelopeMetadata(
sourceServiceId = Recipient.resolved(source).requireServiceId(),
sourceE164 = null,
sourceDeviceId = sourceDeviceId,
sourceDeviceId = 1,
sealedSender = true,
groupId = groupId?.decodedId,
destinationServiceId = Recipient.resolved(destination).requireServiceId()
@@ -64,13 +60,12 @@ object MessageContentFuzzer {
* - An expire timer value
* - Bold style body ranges
*/
fun fuzzTextMessage(sentTimestamp: Long? = null, groupContextV2: GroupContextV2? = null, allowExpireTimeChanges: Boolean = true): Content {
fun fuzzTextMessage(groupContextV2: GroupContextV2? = null): Content {
return Content.Builder()
.dataMessage(
DataMessage.Builder().buildWith {
timestamp = sentTimestamp
body = string()
if (allowExpireTimeChanges && random.nextBoolean()) {
if (random.nextBoolean()) {
expireTimer = random.nextInt(0..28.days.inWholeSeconds.toInt())
}
if (random.nextBoolean()) {
@@ -92,20 +87,6 @@ object MessageContentFuzzer {
.build()
}
/**
* Create an edit message.
*/
fun editTextMessage(targetTimestamp: Long, editedDataMessage: DataMessage): Content {
return Content.Builder()
.editMessage(
EditMessage.Builder().buildWith {
targetSentTimestamp = targetTimestamp
dataMessage = editedDataMessage
}
)
.build()
}
/**
* Create a sync sent text message for the given [DataMessage].
*/
@@ -135,139 +116,6 @@ object MessageContentFuzzer {
).build()
}
/**
* Create a sync reads message for the given [RecipientId] and message timestamp pairings.
*/
fun syncReadsMessage(timestamps: List<Pair<RecipientId, Long>>): Content {
return Content
.Builder()
.syncMessage(
SyncMessage.Builder().buildWith {
read = timestamps.map { (senderId, timestamp) ->
SyncMessage.Read.Builder().buildWith {
this.senderAci = Recipient.resolved(senderId).requireAci().toString()
this.timestamp = timestamp
}
}
}
).build()
}
fun syncDeleteForMeMessage(allDeletes: List<DeleteForMeSync>): Content {
return Content
.Builder()
.syncMessage(
SyncMessage(
deleteForMe = SyncMessage.DeleteForMe(
messageDeletes = allDeletes.map { (conversationId, conversationDeletes) ->
val conversation = Recipient.resolved(conversationId)
SyncMessage.DeleteForMe.MessageDeletes(
conversation = if (conversation.isGroup) {
SyncMessage.DeleteForMe.ConversationIdentifier(threadGroupId = conversation.requireGroupId().decodedId.toByteString())
} else {
SyncMessage.DeleteForMe.ConversationIdentifier(threadServiceId = conversation.requireAci().toString())
},
messages = conversationDeletes.map { (author, timestamp) ->
SyncMessage.DeleteForMe.AddressableMessage(
authorServiceId = Recipient.resolved(author).requireAci().toString(),
sentTimestamp = timestamp
)
}
)
}
)
)
).build()
}
fun syncDeleteForMeConversation(allDeletes: List<DeleteForMeSync>): Content {
return Content
.Builder()
.syncMessage(
SyncMessage(
deleteForMe = SyncMessage.DeleteForMe(
conversationDeletes = allDeletes.map { delete ->
val conversation = Recipient.resolved(delete.conversationId)
SyncMessage.DeleteForMe.ConversationDelete(
conversation = if (conversation.isGroup) {
SyncMessage.DeleteForMe.ConversationIdentifier(threadGroupId = conversation.requireGroupId().decodedId.toByteString())
} else {
SyncMessage.DeleteForMe.ConversationIdentifier(threadServiceId = conversation.requireAci().toString())
},
mostRecentMessages = delete.messages.map { (author, timestamp) ->
SyncMessage.DeleteForMe.AddressableMessage(
authorServiceId = Recipient.resolved(author).requireAci().toString(),
sentTimestamp = timestamp
)
},
mostRecentNonExpiringMessages = delete.nonExpiringMessages.map { (author, timestamp) ->
SyncMessage.DeleteForMe.AddressableMessage(
authorServiceId = Recipient.resolved(author).requireAci().toString(),
sentTimestamp = timestamp
)
},
isFullDelete = delete.isFullDelete
)
}
)
)
).build()
}
fun syncDeleteForMeLocalOnlyConversation(conversations: List<RecipientId>): Content {
return Content
.Builder()
.syncMessage(
SyncMessage(
deleteForMe = SyncMessage.DeleteForMe(
localOnlyConversationDeletes = conversations.map { conversationId ->
val conversation = Recipient.resolved(conversationId)
SyncMessage.DeleteForMe.LocalOnlyConversationDelete(
conversation = if (conversation.isGroup) {
SyncMessage.DeleteForMe.ConversationIdentifier(threadGroupId = conversation.requireGroupId().decodedId.toByteString())
} else {
SyncMessage.DeleteForMe.ConversationIdentifier(threadServiceId = conversation.requireAci().toString())
}
)
}
)
)
).build()
}
fun syncDeleteForMeAttachment(conversationId: RecipientId, message: Pair<RecipientId, Long>, uuid: UUID?, digest: ByteArray?, plainTextHash: String?): Content {
val conversation = Recipient.resolved(conversationId)
return Content
.Builder()
.syncMessage(
SyncMessage(
deleteForMe = SyncMessage.DeleteForMe(
attachmentDeletes = listOf(
SyncMessage.DeleteForMe.AttachmentDelete(
conversation = if (conversation.isGroup) {
SyncMessage.DeleteForMe.ConversationIdentifier(threadGroupId = conversation.requireGroupId().decodedId.toByteString())
} else {
SyncMessage.DeleteForMe.ConversationIdentifier(threadServiceId = conversation.requireAci().toString())
},
targetMessage = SyncMessage.DeleteForMe.AddressableMessage(
authorServiceId = Recipient.resolved(message.first).requireAci().toString(),
sentTimestamp = message.second
),
uuid = uuid?.let { UuidUtil.toByteString(it) },
fallbackDigest = digest?.toByteString(),
fallbackPlaintextHash = plainTextHash?.let { Base64.decodeOrNull(it)?.toByteString() }
)
)
)
)
).build()
}
/**
* Create a random media message that may be:
* - A text body
@@ -336,21 +184,22 @@ object MessageContentFuzzer {
}
/**
* Create a random media message that contains a sticker.
* Create a random media message that can never contain a text body. It may be:
* - A sticker
*/
fun fuzzStickerMediaMessage(sentTimestamp: Long? = null, groupContextV2: GroupContextV2? = null): Content {
fun fuzzMediaMessageNoText(previousMessages: List<TestMessage> = emptyList()): Content {
return Content.Builder()
.dataMessage(
DataMessage.Builder().buildWith {
timestamp = sentTimestamp
sticker = DataMessage.Sticker.Builder().buildWith {
packId = byteString(length = 24)
packKey = byteString(length = 128)
stickerId = random.nextInt()
data_ = attachmentPointer()
emoji = emojis.random(random)
if (random.nextFloat() < 0.9) {
sticker = DataMessage.Sticker.Builder().buildWith {
packId = byteString(length = 24)
packKey = byteString(length = 128)
stickerId = random.nextInt()
data_ = attachmentPointer()
emoji = emojis.random(random)
}
}
groupV2 = groupContextV2
}
).build()
}
@@ -396,7 +245,7 @@ object MessageContentFuzzer {
caption = string(allowNullString = true)
blurHash = string()
uploadTimestamp = random.nextLong()
cdnNumber = 2
cdnNumber = 1
build()
}
@@ -408,14 +257,4 @@ object MessageContentFuzzer {
fun fuzzServerDeliveredTimestamp(envelopeTimestamp: Long): Long {
return envelopeTimestamp + 10
}
data class DeleteForMeSync(
val conversationId: RecipientId,
val messages: List<Pair<RecipientId, Long>>,
val nonExpiringMessages: List<Pair<RecipientId, Long>> = emptyList(),
val isFullDelete: Boolean = true,
val attachments: List<Pair<Long, AttachmentTable.SyncAttachmentId>> = emptyList()
) {
constructor(conversationId: RecipientId, vararg messages: Pair<RecipientId, Long>) : this(conversationId, messages.toList())
}
}

View File

@@ -31,7 +31,7 @@ object MockProvider {
val lockedFailure = PushServiceSocket.RegistrationLockFailure().apply {
svr1Credentials = AuthCredentials.create("username", "password")
svr2Credentials = AuthCredentials.create("username", "password")
svr2Credentials = null
}
val primaryOnlyDeviceList = DeviceInfoList().apply {
@@ -68,7 +68,7 @@ object MockProvider {
}
}
fun createPreKeyResponse(identity: IdentityKeyPair = SignalStore.account.aciIdentityKey, deviceId: Int): PreKeyResponse {
fun createPreKeyResponse(identity: IdentityKeyPair = SignalStore.account().aciIdentityKey, deviceId: Int): PreKeyResponse {
val signedPreKeyRecord = PreKeyUtil.generateSignedPreKey(SecureRandom().nextInt(Medium.MAX_VALUE), identity.privateKey)
val oneTimePreKey = PreKeyRecord(SecureRandom().nextInt(Medium.MAX_VALUE), Curve.generateKeyPair())

View File

@@ -55,5 +55,5 @@ inline fun <reified T> RecordedRequest.parsedRequestBody(): T {
}
private fun defaultRequestPredicate(verb: String, path: String, predicate: RequestPredicate = { true }): RequestPredicate = { request ->
request.method == verb && request.path?.startsWith("/$path") == true && predicate(request)
request.method == verb && request.path.startsWith("/$path") && predicate(request)
}

View File

@@ -19,7 +19,7 @@ import org.thoughtcrime.securesms.crypto.MasterSecretUtil
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil
import org.thoughtcrime.securesms.database.IdentityTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.dependencies.InstrumentationApplicationDependencyProvider
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.profiles.ProfileName
@@ -47,7 +47,7 @@ import java.util.UUID
*/
class SignalActivityRule(private val othersCount: Int = 4, private val createGroup: Boolean = false) : ExternalResource() {
val application: Application = AppDependencies.application
val application: Application = ApplicationDependencies.getApplication()
lateinit var context: Context
private set
@@ -90,8 +90,8 @@ class SignalActivityRule(private val othersCount: Int = 4, private val createGro
val preferences: SharedPreferences = application.getSharedPreferences(MasterSecretUtil.PREFERENCES_NAME, 0)
preferences.edit().putBoolean("passphrase_initialized", true).commit()
SignalStore.account.generateAciIdentityKeyIfNecessary()
SignalStore.account.generatePniIdentityKeyIfNecessary()
SignalStore.account().generateAciIdentityKeyIfNecessary()
SignalStore.account().generatePniIdentityKeyIfNecessary()
val registrationRepository = RegistrationRepository(application)
@@ -111,19 +111,19 @@ class SignalActivityRule(private val othersCount: Int = 4, private val createGro
verifyAccountResponse = VerifyAccountResponse(UUID.randomUUID().toString(), UUID.randomUUID().toString(), false),
masterKey = null,
pin = null,
aciPreKeyCollection = RegistrationRepository.generateSignedAndLastResortPreKeys(SignalStore.account.aciIdentityKey, SignalStore.account.aciPreKeys),
pniPreKeyCollection = RegistrationRepository.generateSignedAndLastResortPreKeys(SignalStore.account.aciIdentityKey, SignalStore.account.pniPreKeys)
aciPreKeyCollection = RegistrationRepository.generateSignedAndLastResortPreKeys(SignalStore.account().aciIdentityKey, SignalStore.account().aciPreKeys),
pniPreKeyCollection = RegistrationRepository.generateSignedAndLastResortPreKeys(SignalStore.account().aciIdentityKey, SignalStore.account().pniPreKeys)
),
false
).blockingGet()
ServiceResponseProcessor.DefaultProcessor(response).resultOrThrow
SignalStore.svr.optOut()
SignalStore.svr().optOut()
RegistrationUtil.maybeMarkRegistrationComplete()
SignalDatabase.recipients.setProfileName(Recipient.self().id, ProfileName.fromParts("Tester", "McTesterson"))
SignalStore.settings.isMessageNotificationsEnabled = false
SignalStore.settings().isMessageNotificationsEnabled = false
return Recipient.self()
}
@@ -141,11 +141,11 @@ class SignalActivityRule(private val othersCount: Int = 4, private val createGro
val recipientId = RecipientId.from(SignalServiceAddress(aci, "+15555551%03d".format(i)))
SignalDatabase.recipients.setProfileName(recipientId, ProfileName.fromParts("Buddy", "#$i"))
SignalDatabase.recipients.setProfileKeyIfAbsent(recipientId, ProfileKeyUtil.createNew())
SignalDatabase.recipients.setCapabilities(recipientId, SignalServiceProfile.Capabilities(true, false))
SignalDatabase.recipients.setCapabilities(recipientId, SignalServiceProfile.Capabilities(true, true, true, true, true, true, true, true, true))
SignalDatabase.recipients.setProfileSharing(recipientId, true)
SignalDatabase.recipients.markRegistered(recipientId, aci)
val otherIdentity = IdentityKeyUtil.generateIdentityKeyPair()
AppDependencies.protocolStore.aci().saveIdentity(SignalProtocolAddress(aci.toString(), 0), otherIdentity.publicKey)
ApplicationDependencies.getProtocolStore().aci().saveIdentity(SignalProtocolAddress(aci.toString(), 0), otherIdentity.publicKey)
others += recipientId
othersKeys += otherIdentity
}
@@ -158,14 +158,14 @@ class SignalActivityRule(private val othersCount: Int = 4, private val createGro
}
fun changeIdentityKey(recipient: Recipient, identityKey: IdentityKey = IdentityKeyUtil.generateIdentityKeyPair().publicKey) {
AppDependencies.protocolStore.aci().saveIdentity(SignalProtocolAddress(recipient.requireServiceId().toString(), 0), identityKey)
ApplicationDependencies.getProtocolStore().aci().saveIdentity(SignalProtocolAddress(recipient.requireServiceId().toString(), 0), identityKey)
}
fun getIdentity(recipient: Recipient): IdentityKey {
return AppDependencies.protocolStore.aci().identities().getIdentity(SignalProtocolAddress(recipient.requireServiceId().toString(), 0))
return ApplicationDependencies.getProtocolStore().aci().identities().getIdentity(SignalProtocolAddress(recipient.requireServiceId().toString(), 0))
}
fun setVerified(recipient: Recipient, status: IdentityTable.VerifiedStatus) {
AppDependencies.protocolStore.aci().identities().setVerified(recipient.id, getIdentity(recipient), IdentityTable.VerifiedStatus.VERIFIED)
ApplicationDependencies.getProtocolStore().aci().identities().setVerified(recipient.id, getIdentity(recipient), IdentityTable.VerifiedStatus.VERIFIED)
}
}

View File

@@ -26,8 +26,8 @@ class SignalDatabaseRule(
override fun starting(description: Description?) {
deleteAllThreads()
SignalStore.account.setAci(localAci)
SignalStore.account.setPni(localPni)
SignalStore.account().setAci(localAci)
SignalStore.account().setPni(localPni)
}
override fun finished(description: Description?) {

View File

@@ -1,9 +0,0 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.testing
@Retention(AnnotationRetention.RUNTIME)
annotation class SignalFlakyTest(val allowedAttempts: Int = 3)

View File

@@ -1,49 +0,0 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.testing
import org.junit.rules.TestRule
import org.junit.runner.Description
import org.junit.runners.model.Statement
import org.signal.core.util.logging.Log
/**
* A JUnit rule that retries tests annotated with [SignalFlakyTest] before considering them to be a failure.
* As the name implies, this is useful for known-flaky tests.
*/
class SignalFlakyTestRule : TestRule {
override fun apply(base: Statement, description: Description): Statement {
val flakyAnnotation = description.getAnnotation(SignalFlakyTest::class.java)
return if (flakyAnnotation != null) {
FlakyStatement(
base = base,
description = description,
allowedAttempts = flakyAnnotation.allowedAttempts
)
} else {
base
}
}
private class FlakyStatement(private val base: Statement, private val description: Description, private val allowedAttempts: Int) : Statement() {
override fun evaluate() {
var attemptsRemaining = allowedAttempts
while (attemptsRemaining > 0) {
try {
base.evaluate()
return
} catch (t: Throwable) {
attemptsRemaining--
if (attemptsRemaining <= 0) {
throw t
}
Log.w(description.testClass.simpleName, "[${description.methodName}] Flaky test failed! $attemptsRemaining attempt(s) remaining.", t)
}
}
}
}
}

View File

@@ -1,14 +1,13 @@
package org.thoughtcrime.securesms.testing
import android.database.Cursor
import org.hamcrest.Matcher
import android.util.Base64
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.hasSize
import org.hamcrest.Matchers.`is`
import org.hamcrest.Matchers.not
import org.hamcrest.Matchers.notNullValue
import org.hamcrest.Matchers.nullValue
import org.signal.core.util.Hex
import org.signal.core.util.logging.Log
import org.signal.core.util.readToList
import org.signal.core.util.select
@@ -57,41 +56,33 @@ infix fun <E, T : Collection<E>> T.assertIsSize(expected: Int) {
assertThat(this, hasSize(expected))
}
infix fun <T : Any> T.assert(matcher: Matcher<T>) {
assertThat(this, matcher)
}
fun CountDownLatch.awaitFor(duration: Duration) {
if (!await(duration.inWholeMilliseconds, TimeUnit.MILLISECONDS)) {
throw TimeoutException("Latch await took longer than ${duration.inWholeMilliseconds}ms")
}
}
fun dumpTableToLogs(tag: String = "TestUtils", table: String, columns: Set<String>? = null) {
dumpTable(table, columns).forEach { Log.d(tag, it.toString()) }
fun dumpTableToLogs(tag: String = "TestUtils", table: String) {
dumpTable(table).forEach { Log.d(tag, it.toString()) }
}
fun dumpTable(table: String, columns: Set<String>?): List<List<Pair<String, String?>>> {
fun dumpTable(table: String): List<List<Pair<String, String?>>> {
return SignalDatabase.rawDatabase
.select()
.from(table)
.run()
.readToList { cursor ->
val map: List<Pair<String, String?>> = cursor.columnNames.mapNotNull { column ->
if (columns == null || columns.contains(column)) {
val index = cursor.getColumnIndex(column)
var data: String? = when (cursor.getType(index)) {
Cursor.FIELD_TYPE_BLOB -> Hex.toStringCondensed(cursor.getBlob(index))
else -> cursor.getString(index)
}
if (table == MessageTable.TABLE_NAME && column == MessageTable.TYPE) {
data = MessageTableTestUtils.typeColumnToString(cursor.getLong(index))
}
column to data
} else {
null
val map: List<Pair<String, String?>> = cursor.columnNames.map { column ->
val index = cursor.getColumnIndex(column)
var data: String? = when (cursor.getType(index)) {
Cursor.FIELD_TYPE_BLOB -> Base64.encodeToString(cursor.getBlob(index), 0)
else -> cursor.getString(index)
}
if (table == MessageTable.TABLE_NAME && column == MessageTable.TYPE) {
data = MessageTableTestUtils.typeColumnToString(cursor.getLong(index))
}
column to data
}
map
}

View File

@@ -0,0 +1,11 @@
package org.thoughtcrime.securesms.util;
/**
* A class that allows us to inject feature flags during tests.
*/
public final class FeatureFlagsAccessor {
public static void forceValue(String key, Object value) {
FeatureFlags.FORCED_VALUES.put(FeatureFlags.PHONE_NUMBER_PRIVACY, true);
}
}

View File

@@ -61,7 +61,7 @@ object MessageTableTestUtils {
isProfileChange:${type == MessageTypes.PROFILE_CHANGE_TYPE}
isGroupV1MigrationEvent:${type == MessageTypes.GV1_MIGRATION_TYPE}
isChangeNumber:${type == MessageTypes.CHANGE_NUMBER_TYPE}
isDonationChannelDonationRequest:${type == MessageTypes.RELEASE_CHANNEL_DONATION_REQUEST_TYPE}
isBoostRequest:${type == MessageTypes.BOOST_REQUEST_TYPE}
isThreadMerge:${type == MessageTypes.THREAD_MERGE_TYPE}
isSmsExport:${type == MessageTypes.SMS_EXPORT_TYPE}
isGroupV2LeaveOnly:${type and MessageTypes.GROUP_V2_LEAVE_BITS == MessageTypes.GROUP_V2_LEAVE_BITS}

View File

@@ -2,9 +2,9 @@ package org.signal.benchmark
import android.content.Context
import org.thoughtcrime.securesms.BuildConfig
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.push.AccountManagerFactory
import org.thoughtcrime.securesms.util.RemoteConfig
import org.thoughtcrime.securesms.util.FeatureFlags
import org.whispersystems.signalservice.api.SignalServiceAccountManager
import org.whispersystems.signalservice.api.account.PreKeyUpload
import org.whispersystems.signalservice.api.push.ServiceId.ACI
@@ -16,15 +16,15 @@ import java.util.Optional
class DummyAccountManagerFactory : AccountManagerFactory() {
override fun createAuthenticated(context: Context, aci: ACI, pni: PNI, number: String, deviceId: Int, password: String): SignalServiceAccountManager {
return DummyAccountManager(
AppDependencies.signalServiceNetworkAccess.getConfiguration(number),
ApplicationDependencies.getSignalServiceNetworkAccess().getConfiguration(number),
aci,
pni,
number,
deviceId,
password,
BuildConfig.SIGNAL_AGENT,
RemoteConfig.okHttpAutomaticRetry,
RemoteConfig.groupLimits.hardLimit
FeatureFlags.okHttpAutomaticRetry(),
FeatureFlags.groupLimits().hardLimit
)
}

View File

@@ -1,6 +1,5 @@
package org.signal.benchmark.setup
import org.thoughtcrime.securesms.attachments.Cdn
import org.thoughtcrime.securesms.attachments.PointerAttachment
import org.thoughtcrime.securesms.database.AttachmentTable
import org.thoughtcrime.securesms.database.MessageType
@@ -10,6 +9,7 @@ import org.thoughtcrime.securesms.mms.IncomingMessage
import org.thoughtcrime.securesms.mms.OutgoingMessage
import org.thoughtcrime.securesms.mms.QuoteModel
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.releasechannel.ReleaseChannel
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId
@@ -144,7 +144,7 @@ object TestMessages {
}
private fun imageAttachment(): SignalServiceAttachmentPointer {
return SignalServiceAttachmentPointer(
Cdn.S3.cdnNumber,
ReleaseChannel.CDN_NUMBER,
SignalServiceAttachmentRemoteId.from(""),
"image/webp",
null,
@@ -161,14 +161,13 @@ object TestMessages {
false,
Optional.empty(),
Optional.empty(),
System.currentTimeMillis(),
null
System.currentTimeMillis()
)
}
private fun voiceAttachment(): SignalServiceAttachmentPointer {
return SignalServiceAttachmentPointer(
Cdn.S3.cdnNumber,
ReleaseChannel.CDN_NUMBER,
SignalServiceAttachmentRemoteId.from(""),
"audio/aac",
null,
@@ -185,8 +184,7 @@ object TestMessages {
false,
Optional.empty(),
Optional.empty(),
System.currentTimeMillis(),
null
System.currentTimeMillis()
)
}

View File

@@ -10,7 +10,7 @@ import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
import org.thoughtcrime.securesms.crypto.MasterSecretUtil
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.net.DeviceTransferBlockingInterceptor
import org.thoughtcrime.securesms.profiles.ProfileName
@@ -35,7 +35,7 @@ object TestUsers {
private var generatedOthers: Int = 0
fun setupSelf(): Recipient {
val application: Application = AppDependencies.application
val application: Application = ApplicationDependencies.getApplication()
DeviceTransferBlockingInterceptor.getInstance().blockNetwork()
PreferenceManager.getDefaultSharedPreferences(application).edit().putBoolean("pref_prompted_push_registration", true).commit()
@@ -44,8 +44,8 @@ object TestUsers {
val preferences: SharedPreferences = application.getSharedPreferences(MasterSecretUtil.PREFERENCES_NAME, 0)
preferences.edit().putBoolean("passphrase_initialized", true).commit()
SignalStore.account.generateAciIdentityKeyIfNecessary()
SignalStore.account.generatePniIdentityKeyIfNecessary()
SignalStore.account().generateAciIdentityKeyIfNecessary()
SignalStore.account().generatePniIdentityKeyIfNecessary()
val registrationRepository = RegistrationRepository(application)
val registrationData = RegistrationData(
@@ -63,8 +63,8 @@ object TestUsers {
VerifyAccountResponse(UUID.randomUUID().toString(), UUID.randomUUID().toString(), false),
masterKey = null,
pin = null,
aciPreKeyCollection = RegistrationRepository.generateSignedAndLastResortPreKeys(SignalStore.account.aciIdentityKey, SignalStore.account.aciPreKeys),
pniPreKeyCollection = RegistrationRepository.generateSignedAndLastResortPreKeys(SignalStore.account.aciIdentityKey, SignalStore.account.pniPreKeys)
aciPreKeyCollection = RegistrationRepository.generateSignedAndLastResortPreKeys(SignalStore.account().aciIdentityKey, SignalStore.account().aciPreKeys),
pniPreKeyCollection = RegistrationRepository.generateSignedAndLastResortPreKeys(SignalStore.account().aciIdentityKey, SignalStore.account().pniPreKeys)
)
AccountManagerFactory.setInstance(DummyAccountManagerFactory())
@@ -77,7 +77,7 @@ object TestUsers {
ServiceResponseProcessor.DefaultProcessor(response).resultOrThrow
SignalStore.svr.optOut()
SignalStore.svr().optOut()
RegistrationUtil.maybeMarkRegistrationComplete()
SignalDatabase.recipients.setProfileName(Recipient.self().id, ProfileName.fromParts("Tester", "McTesterson"))
@@ -100,11 +100,11 @@ object TestUsers {
val recipientId = RecipientId.from(SignalServiceAddress(aci, "+15555551%03d".format(i)))
SignalDatabase.recipients.setProfileName(recipientId, ProfileName.fromParts("Buddy", "#$i"))
SignalDatabase.recipients.setProfileKeyIfAbsent(recipientId, ProfileKeyUtil.createNew())
SignalDatabase.recipients.setCapabilities(recipientId, SignalServiceProfile.Capabilities(true, true))
SignalDatabase.recipients.setCapabilities(recipientId, SignalServiceProfile.Capabilities(true, true, true, true, true, true, true, true, true))
SignalDatabase.recipients.setProfileSharing(recipientId, true)
SignalDatabase.recipients.markRegistered(recipientId, aci)
val otherIdentity = IdentityKeyUtil.generateIdentityKeyPair()
AppDependencies.protocolStore.aci().saveIdentity(SignalProtocolAddress(aci.toString(), 0), otherIdentity.publicKey)
ApplicationDependencies.getProtocolStore().aci().saveIdentity(SignalProtocolAddress(aci.toString(), 0), otherIdentity.publicKey)
others += recipientId
}

View File

@@ -12,7 +12,7 @@ import org.thoughtcrime.securesms.conversation.v2.data.OutgoingTextOnly
import org.thoughtcrime.securesms.database.MessageTypes
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
import org.thoughtcrime.securesms.database.model.StoryType
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.mms.SlideDeck
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel
@@ -123,7 +123,7 @@ class ConversationElementGenerator {
)
val conversationMessage = ConversationMessageFactory.createWithUnresolvedData(
AppDependencies.application,
ApplicationDependencies.getApplication(),
record,
Recipient.UNKNOWN
)

View File

@@ -67,8 +67,7 @@ class InternalConversationTestFragment : Fragment(R.layout.conversation_test_fra
hasWallpaper = springboardViewModel.hasWallpaper.value,
colorizer = Colorizer(),
startExpirationTimeout = {},
chatColorsDataProvider = { ChatColorsDrawable.ChatColorsData(null, null) },
displayDialogFragment = {}
chatColorsDataProvider = { ChatColorsDrawable.ChatColorsData(null, null) }
)
if (springboardViewModel.hasWallpaper.value) {
@@ -231,10 +230,6 @@ class InternalConversationTestFragment : Fragment(R.layout.conversation_test_fra
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
}
override fun onChangeProfileNameUpdateContact(recipient: Recipient) {
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
}
override fun onCallToAction(action: String) {
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
}
@@ -284,7 +279,7 @@ class InternalConversationTestFragment : Fragment(R.layout.conversation_test_fra
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
}
override fun onEditedIndicatorClicked(conversationMessage: ConversationMessage) {
override fun onEditedIndicatorClicked(messageRecord: MessageRecord) {
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
}
@@ -304,14 +299,6 @@ class InternalConversationTestFragment : Fragment(R.layout.conversation_test_fra
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
}
override fun onItemDoubleClick(item: MultiselectPart) {
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
}
override fun onPaymentTombstoneClicked() {
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
}
override fun onShowSafetyTips(forGroup: Boolean) {
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
}

View File

@@ -2,9 +2,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<application
android:usesCleartextTraffic="true"
tools:replace="android:usesCleartextTraffic"

View File

@@ -22,6 +22,8 @@
<uses-permission android:name="android.permission.USE_FINGERPRINT"/>
<uses-permission android:name="org.thoughtcrime.securesms.ACCESS_SECRETS"/>
<uses-permission android:name="android.permission.READ_PROFILE"/>
<uses-permission android:name="android.permission.BROADCAST_WAP_PUSH"
tools:ignore="ProtectedPermissions"/>
<uses-permission android:name="android.permission.READ_CONTACTS"/>
<uses-permission android:name="android.permission.WRITE_CONTACTS"/>
<uses-permission android:name="android.permission.ACCESS_NOTIFICATION_POLICY" />
@@ -43,10 +45,16 @@
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.READ_CALL_STATE"/>
<!-- For sending/receiving events -->
<uses-permission android:name="android.permission.WRITE_CALENDAR"/>
<uses-permission android:name="android.permission.READ_CALENDAR"/>
<!-- Normal -->
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
@@ -63,14 +71,17 @@
<uses-permission android:name="android.permission.INSTALL_SHORTCUT"/>
<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" />
<!-- For device transfer -->
<!-- For fixing MMS -->
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<!-- Set image as wallpaper -->
<uses-permission android:name="android.permission.SET_WALLPAPER"/>
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BROADCAST_STICKY" />
<uses-permission android:name="android.permission.CALL_PHONE" />
<uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
<uses-permission android:name="android.permission.RAISED_THREAD_PRIORITY" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
@@ -83,15 +94,6 @@
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED" />
<!-- For services -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_REMOTE_MESSAGING" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
<!-- For vestigial telecom integration service -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_PHONE_CALL" />
<application android:name=".ApplicationContext"
android:icon="@mipmap/ic_launcher"
@@ -124,32 +126,19 @@
<meta-data android:name="firebase_analytics_collection_deactivated" android:value="true" />
<meta-data android:name="google_analytics_adid_collection_enabled" android:value="false" />
<meta-data android:name="firebase_messaging_auto_init_enabled" android:value="false" />
<meta-data android:name="android.webkit.WebView.MetricsOptOut" android:value="true" />
<activity android:name=".WebRtcCallActivity"
android:theme="@style/TextSecure.DarkTheme.WebRTCCall"
android:excludeFromRecents="true"
android:supportsPictureInPicture="true"
android:windowSoftInputMode="stateAlwaysHidden"
android:configChanges="screenSize|smallestScreenSize|screenLayout"
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"
android:taskAffinity=".calling"
android:resizeableActivity="true"
android:launchMode="singleTask"
android:exported="false" />
<activity
android:name=".components.webrtc.v2.CallActivity"
android:configChanges="screenSize|smallestScreenSize|screenLayout"
android:excludeFromRecents="true"
android:exported="true"
android:launchMode="singleTask"
android:resizeableActivity="true"
android:supportsPictureInPicture="true"
android:taskAffinity=".calling"
android:theme="@style/TextSecure.DarkTheme.WebRTCCall"
android:windowSoftInputMode="stateAlwaysHidden" />
<activity android:name=".messagerequests.CalleeMustAcceptMessageRequestActivity"
<activity android:name=".messagerequests.CalleeMustAcceptMessageRequestActivity"
android:theme="@style/TextSecure.DarkNoActionBar"
android:noHistory="true"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
@@ -185,6 +174,10 @@
</intent-filter>
</activity>
<activity android:name=".preferences.MmsPreferencesActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:exported="false" />
<activity android:name=".sharing.interstitial.ShareInterstitialActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:windowSoftInputMode="adjustResize"
@@ -632,7 +625,7 @@
<activity android:name=".conversation.v2.ConversationActivity"
android:windowSoftInputMode="stateUnchanged"
android:launchMode="singleTask"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:parentActivityName=".MainActivity"
android:resizeableActivity="true"
android:exported="false">
@@ -683,7 +676,7 @@
<activity android:name=".PassphrasePromptActivity"
android:launchMode="singleTask"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:theme="@style/TextSecure.LightIntroTheme"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:exported="false"/>
@@ -726,7 +719,6 @@
android:theme="@style/TextSecure.DarkNoActionBar"
android:windowSoftInputMode="stateAlwaysHidden|adjustNothing"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"
android:exported="false"/>
@@ -770,6 +762,13 @@
android:windowSoftInputMode="stateAlwaysHidden"
android:exported="false"/>
<activity
android:name=".backup.v2.ui.MessageBackupsFlowActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:exported="false"
android:theme="@style/Signal.DayNight.NoActionBar"
android:windowSoftInputMode="adjustResize" />
<activity
android:name=".stories.settings.StorySettingsActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
@@ -812,6 +811,13 @@
android:windowSoftInputMode="stateAlwaysHidden"
android:exported="false"/>
<activity
android:name=".badges.gifts.flow.GiftFlowActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|screenLayout|screenSize"
android:theme="@style/Signal.DayNight.NoActionBar"
android:windowSoftInputMode="stateAlwaysHidden"
android:exported="false"/>
<activity
android:name=".wallpaper.ChatWallpaperActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
@@ -838,14 +844,7 @@
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:exported="false"/>
<activity android:name=".registration.ui.RegistrationActivity"
android:launchMode="singleTask"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:windowSoftInputMode="stateHidden"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:exported="false"/>
<activity android:name=".restore.RestoreActivity"
<activity android:name=".registration.RegistrationNavigationActivity"
android:launchMode="singleTask"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:windowSoftInputMode="stateHidden"
@@ -941,25 +940,19 @@
</activity>
<activity android:name="org.thoughtcrime.securesms.webrtc.VoiceCallShare"
android:exported="true"
android:excludeFromRecents="true"
android:permission="android.permission.CALL_PHONE"
android:theme="@style/NoAnimation.Theme.BlackScreen"
android:launchMode="singleTask"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize">
android:exported="true"
android:excludeFromRecents="true"
android:permission="android.permission.CALL_PHONE"
android:theme="@style/NoAnimation.Theme.BlackScreen"
android:launchMode="singleTask"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize">
<intent-filter tools:ignore="AppLinkUrlError">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="vnd.android.cursor.item/vnd.org.thoughtcrime.securesms.call" />
</intent-filter>
<intent-filter tools:ignore="AppLinkUrlError">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="vnd.android.cursor.item/vnd.org.thoughtcrime.securesms.videocall" />
</intent-filter>
</activity>
<activity android:name=".mediasend.AvatarSelectionActivity"
@@ -983,16 +976,12 @@
android:windowSoftInputMode="stateVisible|adjustResize"
android:exported="false"/>
<activity android:name=".registration.ui.restore.RemoteRestoreActivity"
<activity android:name=".profiles.username.AddAUsernameActivity"
android:theme="@style/Signal.DayNight.NoActionBar"
android:exported="false"/>
<activity android:name=".profiles.manage.EditProfileActivity"
android:theme="@style/TextSecure.LightTheme"
android:windowSoftInputMode="stateVisible|adjustResize"
android:exported="false"/>
<activity android:name=".nicknames.NicknameActivity"
<activity android:name=".profiles.manage.EditProfileActivity"
android:theme="@style/TextSecure.LightTheme"
android:windowSoftInputMode="stateVisible|adjustResize"
android:exported="false"/>
@@ -1100,43 +1089,30 @@
android:theme="@style/Theme.Signal.WallpaperCropper"
android:exported="false"/>
<activity android:name=".components.settings.app.usernamelinks.main.QrImageSelectionActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:theme="@style/TextSecure.DarkNoActionBar"
android:exported="false"/>
<activity android:name=".components.settings.app.usernamelinks.main.UsernameQrScannerActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:exported="false"/>
<activity android:name=".reactions.edit.EditReactionsActivity"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:exported="false"/>
<activity android:name=".components.settings.app.subscription.donate.CheckoutFlowActivity"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:windowSoftInputMode="adjustResize"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:exported="false"/>
<activity android:name=".components.settings.app.subscription.donate.DonateToSignalActivity"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:exported="false"/>
<service
android:enabled="true"
android:name=".service.webrtc.WebRtcCallService"
android:foregroundServiceType="dataSync"
android:foregroundServiceType="camera|microphone"
android:exported="false"/>
<service
android:enabled="true"
android:exported="false"
android:name=".service.KeyCachingService"
android:foregroundServiceType="remoteMessaging"/>
android:name=".service.KeyCachingService" />
<service
android:enabled="true"
android:name=".messages.IncomingMessageObserver$ForegroundService"
android:foregroundServiceType="remoteMessaging"
android:exported="false"/>
<service
@@ -1147,7 +1123,6 @@
<service
android:name=".service.webrtc.AndroidCallConnectionService"
android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE"
android:foregroundServiceType="phoneCall"
android:exported="true">
<intent-filter>
<action android:name="android.telecom.ConnectionService" />
@@ -1170,6 +1145,19 @@
</intent-filter>
</receiver>
<service android:name=".service.QuickResponseService"
android:permission="android.permission.SEND_RESPOND_VIA_MESSAGE"
android:exported="true" >
<intent-filter>
<action android:name="android.intent.action.RESPOND_VIA_MESSAGE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="sms" />
<data android:scheme="smsto" />
<data android:scheme="mms" />
<data android:scheme="mmsto" />
</intent-filter>
</service>
<service android:name=".service.AccountAuthenticatorService" android:exported="true">
<intent-filter>
<action android:name="android.accounts.AccountAuthenticator" />
@@ -1187,17 +1175,10 @@
<service
android:name=".service.GenericForegroundService"
android:foregroundServiceType="dataSync"
android:exported="false"/>
<service
android:name=".service.AttachmentProgressService"
android:foregroundServiceType="dataSync"
android:exported="false"/>
<service
android:name=".service.BackupProgressService"
android:foregroundServiceType="dataSync"
android:exported="false"/>
<service
@@ -1206,7 +1187,6 @@
<service
android:name=".gcm.FcmFetchForegroundService"
android:foregroundServiceType="remoteMessaging"
android:exported="false"/>
<service android:name=".gcm.FcmReceiveService" android:exported="true">
@@ -1215,6 +1195,13 @@
</intent-filter>
</service>
<receiver android:name=".service.SmsDeliveryListener"
android:exported="true">
<intent-filter>
<action android:name="org.thoughtcrime.securesms.services.MESSAGE_SENT"/>
</intent-filter>
</receiver>
<receiver android:name=".notifications.MarkReadReceiver"
android:enabled="true"
android:exported="false">
@@ -1274,6 +1261,11 @@
android:exported="false"
android:grantUriPermissions="true" />
<provider android:name=".providers.MmsBodyProvider"
android:grantUriPermissions="true"
android:exported="false"
android:authorities="${applicationId}.mms" />
<provider android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
@@ -1321,12 +1313,6 @@
</intent-filter>
</receiver>
<receiver android:name=".service.AnalyzeDatabaseAlarmListener" android:exported="false">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<receiver android:name="org.thoughtcrime.securesms.jobs.ForegroundServiceUtil$Receiver" android:exported="false" />
<receiver android:name=".service.PersistentConnectionBootListener" android:exported="false">
@@ -1391,11 +1377,7 @@
</intent-filter>
</receiver>
<service
android:name="org.thoughtcrime.securesms.service.webrtc.ActiveCallManager$ActiveCallForegroundService"
android:exported="false"
android:foregroundServiceType="dataSync|microphone" />
<service android:name="org.thoughtcrime.securesms.service.webrtc.ActiveCallManager$ActiveCallForegroundService" android:exported="false" />
<receiver android:name="org.thoughtcrime.securesms.service.webrtc.ActiveCallManager$ActiveCallServiceReceiver" android:exported="false">
<intent-filter>
<action android:name="org.thoughtcrime.securesms.service.webrtc.ActiveCallAction.DENY"/>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 127 KiB

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 176 KiB

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 127 KiB

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 209 KiB

After

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 137 KiB

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 194 KiB

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 213 KiB

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 210 KiB

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 221 KiB

After

Width:  |  Height:  |  Size: 147 KiB

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