Compare commits

..

2 Commits

Author SHA1 Message Date
Greyson Parrelli
5cad031b28 Bump version to 5.24.7.1 2021-10-02 16:04:37 -04:00
Greyson Parrelli
761657ddc0 Improve handling of badly-serialized data.
h/t @i-infra
2021-10-02 16:04:37 -04:00
3380 changed files with 58491 additions and 252682 deletions

View File

@@ -50,5 +50,5 @@ Describe here the issue that you are experiencing.
**Signal version:** 0.0.0
### Link to debug log
<!-- immediately after the bug has happened capture a debug log via Signal's settings (Help -> Debug log) and paste the link below -->
<!-- immediately after the bug has happened capture a debug log via Signal's advanced settings and paste the link below -->

23
.github/stale.yml vendored
View File

@@ -1,23 +0,0 @@
# Number of days of inactivity before an Issue or Pull Request becomes stale
daysUntilStale: 60
# Number of days of inactivity before an Issue or Pull Request with the stale label is closed.
# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale.
daysUntilClose: 7
issues:
exemptLabels:
- acknowledged
# Comment to post when marking as stale. Set to `false` to disable
markComment: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. Thank you
for your contributions.
# Comment to post when closing a stale Issue or Pull Request.
closeComment: >
This issue has been closed due to inactivity.
# Limit the number of actions per hour, from 1-30. Default is 30
limitPerRun: 1

View File

@@ -4,7 +4,7 @@ on:
pull_request:
push:
branches:
- 'main'
- 'master'
- '4.**'
- '5.**'
@@ -14,12 +14,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v2
- name: set up JDK 11
uses: actions/setup-java@v3
uses: actions/setup-java@v1
with:
distribution: temurin
java-version: 11
- name: Validate Gradle Wrapper
@@ -33,7 +32,7 @@ jobs:
- name: Archive reports for failed build
if: ${{ failure() }}
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v2
with:
name: reports
path: '*/build/reports'

View File

@@ -10,9 +10,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v2
- name: Build image
run: cd reproducible-builds && docker build -t signal-android . && cd ..
- name: Test build
run: docker run --rm -v $(pwd):/project -w /project signal-android ./gradlew clean assemblePlayProdRelease
run: docker run --rm -v $(pwd):/project -w /project signal-android ./gradlew clean assembleRelease

View File

@@ -43,21 +43,12 @@
</JavaCodeStyleSettings>
<JetCodeStyleSettings>
<option name="PACKAGES_TO_USE_STAR_IMPORTS">
<value>
<package name="kotlinx.android.synthetic" alias="false" withSubpackages="true" />
<package name="io.ktor" alias="false" withSubpackages="true" />
</value>
<value />
</option>
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="2147483647" />
<option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="2147483647" />
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>
<codeStyleSettings language="HTML">
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="2" />
<option name="TAB_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="JAVA">
<option name="BRACE_STYLE" value="5" />
<option name="CLASS_BRACE_STYLE" value="5" />
@@ -222,5 +213,13 @@
</rules>
</arrangement>
</codeStyleSettings>
<codeStyleSettings language="kotlin">
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="4" />
<option name="TAB_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
</code_scheme>
</component>

View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExportableFileTemplateSettings">
<default_templates>
<template name="ViewModel.kt" file-name="${NAME}ViewModel" reformat="true" live-template-enabled="false" />
</default_templates>
</component>
</project>

View File

@@ -1,20 +0,0 @@
#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME}
import androidx.lifecycle.ViewModel
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.disposables.CompositeDisposable
import org.thoughtcrime.securesms.util.rx.RxStore
#end
#parse("File Header.java")
class ${NAME}ViewModel : ViewModel() {
private val store = RxStore(${NAME}State())
private val disposables = CompositeDisposable()
val state: Flowable<${NAME}State> = store.stateFlowable
override fun onCleared() {
disposables.clear()
}
}

View File

@@ -28,7 +28,7 @@ https://www.transifex.com/projects/p/signal-android/
## Contributing Code
If you're new to the Signal codebase, we recommend going through our issues and picking out a simple bug to fix (check the "easy" label in our issues) in order to get yourself familiar. Also please have a look at the [CONTRIBUTING.md](https://github.com/signalapp/Signal-Android/blob/main/CONTRIBUTING.md), that might answer some of your questions.
If you're new to the Signal codebase, we recommend going through our issues and picking out a simple bug to fix (check the "easy" label in our issues) in order to get yourself familiar. Also please have a look at the [CONTRIBUTING.md](https://github.com/signalapp/Signal-Android/blob/master/CONTRIBUTING.md), that might answer some of your questions.
For larger changes and feature ideas, we ask that you propose it on the [unofficial Community Forum](https://community.signalusers.org) for a high-level discussion with the wider community before implementation.
@@ -59,7 +59,7 @@ The form and manner of this distribution makes it eligible for export under the
## License
Copyright 2013-2022 Signal
Copyright 2013-2021 Signal
Licensed under the GPLv3: http://www.gnu.org/licenses/gpl-3.0.html

View File

@@ -96,7 +96,7 @@ try:
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:
except sqlite3.Error, e:
if connection:
connection.rollback()
print("Error: %s" % e.args[0])

View File

@@ -1,21 +1,34 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
apply plugin: 'com.google.protobuf'
apply plugin: 'androidx.navigation.safeargs'
apply plugin: 'witness'
apply plugin: 'org.jlleitschuh.gradle.ktlint'
apply from: 'translations.gradle'
apply from: 'witness-verifications.gradle'
apply plugin: 'org.jetbrains.kotlin.android'
apply plugin: 'app.cash.exhaustive'
apply plugin: 'kotlin-parcelize'
apply from: 'static-ips.gradle'
repositories {
maven {
url "https://raw.github.com/signalapp/maven/master/circular-progress-button/releases/"
content {
includeGroupByRegex "com\\.github\\.dmytrodanylyk\\.circular-progress-button\\.*"
}
}
maven {
url "https://raw.github.com/signalapp/maven/master/sqlcipher/release/"
content {
includeGroupByRegex "org\\.signal.*"
}
}
maven { // textdrawable
url 'https://dl.bintray.com/amulyakhare/maven'
content {
includeGroupByRegex "com\\.amulyakhare.*"
}
}
maven {
url "https://www.jitpack.io"
}
@@ -32,6 +45,7 @@ repositories {
includeVersion "cn.carbswang.android", "NumberPickerView", "1.0.9"
includeVersion "com.takisoft.fix", "colorpicker", "0.9.1"
includeVersion "com.codewaves.stickyheadergrid", "stickyheadergrid", "0.9.4"
includeVersion "com.amulyakhare", "com.amulyakhare.textdrawable", "1.0.1"
includeVersion "com.google.android", "flexbox", "0.3.0"
}
}
@@ -39,7 +53,7 @@ repositories {
protobuf {
protoc {
artifact = 'com.google.protobuf:protoc:3.18.0'
artifact = 'com.google.protobuf:protoc:3.10.0'
}
generateProtoTasks {
all().each { task ->
@@ -52,36 +66,40 @@ protobuf {
}
}
ktlint {
// Use a newer version to resolve https://github.com/JLLeitschuh/ktlint-gradle/issues/507
version = "0.43.2"
}
def canonicalVersionCode = 1091
def canonicalVersionName = "5.43.5"
def canonicalVersionCode = 927
def canonicalVersionName = "5.24.7.1"
def postFixSize = 100
def abiPostFix = ['universal' : 0,
'armeabi-v7a' : 1,
'arm64-v8a' : 2,
'x86' : 3,
'x86_64' : 4]
def abiPostFix = ['universal' : 5,
'armeabi-v7a' : 6,
'arm64-v8a' : 7,
'x86' : 8,
'x86_64' : 9]
def keystores = [ 'debug' : loadKeystoreProperties('keystore.debug.properties') ]
def selectableVariants = [
'nightlyProdSpinner',
'internalProdFlipper',
'internalProdPerf',
'internalProdRelease',
'internalStagingFlipper',
'internalStagingPerf',
'internalStagingRelease',
'nightlyProdFlipper',
'nightlyProdPerf',
'nightlyProdRelease',
'nightlyStagingPerf',
'playProdDebug',
'playProdSpinner',
'playProdFlipper',
'playProdPerf',
'playProdRelease',
'playStagingDebug',
'playStagingSpinner',
'playStagingFlipper',
'playStagingPerf',
'playStagingRelease',
'websiteProdSpinner',
'studyProdMock',
'studyProdPerf',
'websiteProdFlipper',
'websiteProdRelease',
]
@@ -112,50 +130,6 @@ android {
}
}
testOptions {
execution 'ANDROIDX_TEST_ORCHESTRATOR'
unitTests {
includeAndroidResources = true
}
}
lintOptions {
checkReleaseBuilds false
abortOnError true
baseline file("lint-baseline.xml")
disable "LintError"
}
sourceSets {
test {
java.srcDirs += "$projectDir/src/testShared"
}
androidTest {
java.srcDirs += "$projectDir/src/testShared"
}
}
compileOptions {
coreLibraryDesugaringEnabled true
sourceCompatibility JAVA_VERSION
targetCompatibility JAVA_VERSION
}
packagingOptions {
exclude 'LICENSE.txt'
exclude 'LICENSE'
exclude 'NOTICE'
exclude 'asm-license.txt'
exclude 'META-INF/LICENSE'
exclude 'META-INF/NOTICE'
exclude 'META-INF/proguard/androidx-annotations.pro'
exclude 'libsignal_jni.dylib'
exclude 'signal_jni.dll'
}
defaultConfig {
versionCode canonicalVersionCode * postFixSize
versionName canonicalVersionName
@@ -170,12 +144,11 @@ android {
buildConfigField "long", "BUILD_TIMESTAMP", getLastCommitTimestamp() + "L"
buildConfigField "String", "GIT_HASH", "\"${getGitHash()}\""
buildConfigField "String", "SIGNAL_URL", "\"https://chat.signal.org\""
buildConfigField "String", "SIGNAL_URL", "\"https://textsecure-service.whispersystems.org\""
buildConfigField "String", "STORAGE_URL", "\"https://storage.signal.org\""
buildConfigField "String", "SIGNAL_CDN_URL", "\"https://cdn.signal.org\""
buildConfigField "String", "SIGNAL_CDN2_URL", "\"https://cdn2.signal.org\""
buildConfigField "String", "SIGNAL_CONTACT_DISCOVERY_URL", "\"https://api.directory.signal.org\""
buildConfigField "String", "SIGNAL_CDSI_URL", "\"https://cdsi.staging.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_SFU_URL", "\"https://sfu.voip.signal.org\""
@@ -183,35 +156,24 @@ android {
buildConfigField "String[]", "SIGNAL_SFU_INTERNAL_URLS", "new String[]{\"https://sfu.test.voip.signal.org\", \"https://sfu.staging.voip.signal.org\"}"
buildConfigField "String", "CONTENT_PROXY_HOST", "\"contentproxy.signal.org\""
buildConfigField "int", "CONTENT_PROXY_PORT", "443"
buildConfigField "String[]", "SIGNAL_SERVICE_IPS", service_ips
buildConfigField "String[]", "SIGNAL_STORAGE_IPS", storage_ips
buildConfigField "String[]", "SIGNAL_CDN_IPS", cdn_ips
buildConfigField "String[]", "SIGNAL_CDN2_IPS", cdn2_ips
buildConfigField "String[]", "SIGNAL_CDS_IPS", cds_ips
buildConfigField "String[]", "SIGNAL_KBS_IPS", kbs_ips
buildConfigField "String[]", "SIGNAL_SFU_IPS", sfu_ips
buildConfigField "String[]", "SIGNAL_CONTENT_PROXY_IPS", content_proxy_ips
buildConfigField "String", "SIGNAL_AGENT", "\"OWA\""
buildConfigField "String", "CDS_MRENCLAVE", "\"c98e00a4e3ff977a56afefe7362a27e4961e4f19e211febfbb19b897e6b80b15\""
buildConfigField "String", "CDSI_MRENCLAVE", "\"42e36b74794abe612d698308b148ff8a7dc5fdc6ad28d99bc5024ed6ece18dfe\""
buildConfigField "org.thoughtcrime.securesms.KbsEnclave", "KBS_ENCLAVE", "new org.thoughtcrime.securesms.KbsEnclave(\"0cedba03535b41b67729ce9924185f831d7767928a1d1689acb689bc079c375f\", " +
"\"187d2739d22be65e74b65f0055e74d31310e4267e5fac2b1246cc8beba81af39\", " +
"\"ee19f1965b1eefa3dc4204eb70c04f397755f771b8c1909d080c04dad2a6a9ba\")"
buildConfigField "org.thoughtcrime.securesms.KbsEnclave[]", "KBS_FALLBACKS", "new org.thoughtcrime.securesms.KbsEnclave[0]"
buildConfigField "KbsEnclave", "KBS_ENCLAVE", "new KbsEnclave(\"fe7c1bfae98f9b073d220366ea31163ee82f6d04bead774f71ca8e5c40847bfe\"," +
"\"fe7c1bfae98f9b073d220366ea31163ee82f6d04bead774f71ca8e5c40847bfe\", " +
"\"a3baab19ef6ce6f34ab9ebb25ba722725ae44a8872dc0ff08ad6d83a9489de87\")";
buildConfigField "KbsEnclave[]", "KBS_FALLBACKS", "new KbsEnclave[0]"
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+ikU5gBXsRmoF94GXQ==\""
buildConfigField "String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"AMhf5ywVwITZMsff/eCyudZx9JDmkkkbV6PInzG4p8x3VqVJSFiMvnvlEKWuRob/1eaIetR31IYeAbm0NdOuHH8Qi+Rexi1wLlpzIo1gstHWBfZzy1+qHRV5A4TqPp15YzBPm0WSggW6PbSn+F4lf57VCnHF7p8SvzAA2ZZJPYJURt8X7bbg+H3i+PEjH9DXItNEqs2sNcug37xZQDLm7X0=\""
buildConfigField "String[]", "LANGUAGES", "new String[]{\"" + autoResConfig().collect { s -> s.replace('-r', '_') }.join('", "') + '"}'
buildConfigField "int", "CANONICAL_VERSION_CODE", "$canonicalVersionCode"
buildConfigField "String", "DEFAULT_CURRENCIES", "\"EUR,AUD,GBP,CAD,CNY\""
buildConfigField "int[]", "MOBILE_COIN_REGIONS", "new int[]{44,49,33,41}"
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 "String", "BUILD_DISTRIBUTION_TYPE", "\"unset\""
buildConfigField "String", "BUILD_ENVIRONMENT_TYPE", "\"unset\""
buildConfigField "String", "BUILD_VARIANT_TYPE", "\"unset\""
buildConfigField "String", "BADGE_STATIC_ROOT", "\"https://updates2.signal.org/static/badges/\""
buildConfigField "String", "STRIPE_PUBLISHABLE_KEY", "\"pk_live_6cmGZopuTsV8novGgJJW9JpC00vLIgtQ1D\""
ndk {
abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
@@ -229,7 +191,24 @@ android {
}
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
testInstrumentationRunnerArguments clearPackageData: 'true'
}
compileOptions {
coreLibraryDesugaringEnabled true
sourceCompatibility JAVA_VERSION
targetCompatibility JAVA_VERSION
}
packagingOptions {
exclude 'LICENSE.txt'
exclude 'LICENSE'
exclude 'NOTICE'
exclude 'asm-license.txt'
exclude 'META-INF/LICENSE'
exclude 'META-INF/NOTICE'
exclude 'META-INF/proguard/androidx-annotations.pro'
exclude '/org/spongycastle/x509/CertPathReviewerMessages.properties'
exclude '/org/spongycastle/x509/CertPathReviewerMessages_de.properties'
}
buildTypes {
@@ -238,7 +217,7 @@ android {
signingConfig signingConfigs.debug
}
isDefault true
minifyEnabled false
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'),
'proguard/proguard-firebase-messaging.pro',
'proguard/proguard-google-play-services.pro',
@@ -247,6 +226,7 @@ android {
'proguard/proguard-appcompat-v7.pro',
'proguard/proguard-square-okhttp.pro',
'proguard/proguard-square-okio.pro',
'proguard/proguard-spongycastle.pro',
'proguard/proguard-rounded-image-view.pro',
'proguard/proguard-glide.pro',
'proguard/proguard-shortcutbadger.pro',
@@ -262,12 +242,12 @@ android {
buildConfigField "String", "BUILD_VARIANT_TYPE", "\"Debug\""
}
spinner {
flipper {
initWith debug
isDefault false
minifyEnabled false
matchingFallbacks = ['debug']
buildConfigField "String", "BUILD_VARIANT_TYPE", "\"Spinner\""
buildConfigField "String", "BUILD_VARIANT_TYPE", "\"Flipper\""
}
release {
minifyEnabled true
@@ -278,10 +258,16 @@ android {
initWith debug
isDefault false
debuggable false
minifyEnabled true
matchingFallbacks = ['debug']
buildConfigField "String", "BUILD_VARIANT_TYPE", "\"Perf\""
}
mock {
initWith debug
isDefault false
minifyEnabled false
matchingFallbacks = ['debug']
buildConfigField "String", "BUILD_VARIANT_TYPE", "\"Mock\""
}
}
productFlavors {
@@ -302,6 +288,14 @@ android {
buildConfigField "String", "BUILD_DISTRIBUTION_TYPE", "\"website\""
}
internal {
dimension 'distribution'
ext.websiteUpdateUrl = "null"
buildConfigField "boolean", "PLAY_STORE_DISABLED", "false"
buildConfigField "String", "NOPLAY_UPDATE_URL", "$ext.websiteUpdateUrl"
buildConfigField "String", "BUILD_DISTRIBUTION_TYPE", "\"internal\""
}
nightly {
dimension 'distribution'
versionNameSuffix "-nightly-untagged-${getDateSuffix()}"
@@ -311,6 +305,16 @@ android {
buildConfigField "String", "BUILD_DISTRIBUTION_TYPE", "\"nightly\""
}
study {
dimension 'distribution'
applicationIdSuffix ".study"
ext.websiteUpdateUrl = "null"
buildConfigField "boolean", "PLAY_STORE_DISABLED", "false"
buildConfigField "String", "NOPLAY_UPDATE_URL", "$ext.websiteUpdateUrl"
buildConfigField "String", "BUILD_DISTRIBUTION_TYPE", "\"study\""
}
prod {
dimension 'environment'
@@ -325,25 +329,23 @@ android {
applicationIdSuffix ".staging"
buildConfigField "String", "SIGNAL_URL", "\"https://chat.staging.signal.org\""
buildConfigField "String", "SIGNAL_URL", "\"https://textsecure-service-staging.whispersystems.org\""
buildConfigField "String", "STORAGE_URL", "\"https://storage-staging.signal.org\""
buildConfigField "String", "SIGNAL_CDN_URL", "\"https://cdn-staging.signal.org\""
buildConfigField "String", "SIGNAL_CDN2_URL", "\"https://cdn2-staging.signal.org\""
buildConfigField "String", "SIGNAL_CONTACT_DISCOVERY_URL", "\"https://api-staging.directory.signal.org\""
buildConfigField "String", "SIGNAL_KEY_BACKUP_URL", "\"https://api-staging.backup.signal.org\""
buildConfigField "String", "CDS_MRENCLAVE", "\"c98e00a4e3ff977a56afefe7362a27e4961e4f19e211febfbb19b897e6b80b15\""
buildConfigField "org.thoughtcrime.securesms.KbsEnclave", "KBS_ENCLAVE", "new org.thoughtcrime.securesms.KbsEnclave(\"dd6f66d397d9e8cf6ec6db238e59a7be078dd50e9715427b9c89b409ffe53f99\", " +
"\"4200003414528c151e2dccafbc87aa6d3d66a5eb8f8c05979a6e97cb33cd493a\", " +
"\"ee19f1965b1eefa3dc4204eb70c04f397755f771b8c1909d080c04dad2a6a9ba\")"
buildConfigField "org.thoughtcrime.securesms.KbsEnclave[]", "KBS_FALLBACKS", "new org.thoughtcrime.securesms.KbsEnclave[0]"
buildConfigField "KbsEnclave", "KBS_ENCLAVE", "new KbsEnclave(\"823a3b2c037ff0cbe305cc48928cfcc97c9ed4a8ca6d49af6f7d6981fb60a4e9\", " +
"\"16b94ac6d2b7f7b9d72928f36d798dbb35ed32e7bb14c42b4301ad0344b46f29\", " +
"\"a3baab19ef6ce6f34ab9ebb25ba722725ae44a8872dc0ff08ad6d83a9489de87\")"
buildConfigField "KbsEnclave[]", "KBS_FALLBACKS", "new KbsEnclave[0]"
buildConfigField "String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx\""
buildConfigField "String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"ABSY21VckQcbSXVNCGRYJcfWHiAMZmpTtTELcDmxgdFbtp/bWsSxZdMKzfCp8rvIs8ocCU3B37fT3r4Mi5qAemeGeR2X+/YmOGR5ofui7tD5mDQfstAI9i+4WpMtIe8KC3wU5w3Inq3uNWVmoGtpKndsNfwJrCg0Hd9zmObhypUnSkfYn2ooMOOnBpfdanRtrvetZUayDMSC5iSRcXKpdlukrpzzsCIvEwjwQlJYVPOQPj4V0F4UXXBdHSLK05uoPBCQG8G9rYIGedYsClJXnbrgGYG3eMTG5hnx4X4ntARBgELuMWWUEEfSK0mjXg+/2lPmWcTZWR9nkqgQQP0tbzuiPm74H2wMO4u1Wafe+UwyIlIT9L7KLS19Aw8r4sPrXQ==\""
buildConfigField "String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"ABSY21VckQcbSXVNCGRYJcfWHiAMZmpTtTELcDmxgdFbtp/bWsSxZdMKzfCp8rvIs8ocCU3B37fT3r4Mi5qAemeGeR2X+/YmOGR5ofui7tD5mDQfstAI9i+4WpMtIe8KC3wU5w3Inq3uNWVmoGtpKndsNfwJrCg0Hd9zmObhypUnSkfYn2ooMOOnBpfdanRtrvetZUayDMSC5iSRcXKpdls=\""
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 "String", "BUILD_ENVIRONMENT_TYPE", "\"Staging\""
buildConfigField "String", "STRIPE_PUBLISHABLE_KEY", "\"pk_test_sngOd8FnXNkpce9nPXawKrJD00kIDngZkD\""
}
}
@@ -353,9 +355,6 @@ android {
output.versionCodeOverride = canonicalVersionCode * postFixSize + 5
def tag = getCurrentGitTag()
if (tag != null && tag.length() > 0) {
if (tag.startsWith("v")) {
tag = tag.substring(1)
}
output.versionNameOverride = tag
}
} else {
@@ -380,6 +379,18 @@ android {
variant.setIgnore(true)
}
}
lintOptions {
abortOnError true
baseline file("lint-baseline.xml")
disable "LintError"
}
testOptions {
unitTests {
includeAndroidResources = true
}
}
}
dependencies {
@@ -437,15 +448,12 @@ dependencies {
implementation project(':libsignal-service')
implementation project(':paging')
implementation project(':core-util')
implementation project(':glide-config')
implementation project(':video')
implementation project(':device-transfer')
implementation project(':image-editor')
implementation project(':donations')
implementation project(':contacts')
implementation project(':qr')
implementation libs.libsignal.android
implementation libs.signal.zkgroup.android
implementation libs.signal.client.android
implementation libs.google.protobuf.javalite
implementation(libs.mobilecoin) {
@@ -466,12 +474,16 @@ dependencies {
implementation libs.apache.httpclient.android
implementation libs.photoview
implementation libs.glide.glide
kapt libs.glide.compiler
kapt libs.androidx.annotation
implementation libs.roundedimageview
implementation libs.materialish.progress
implementation libs.greenrobot.eventbus
implementation libs.waitingdots
implementation libs.floatingactionbutton
implementation libs.google.zxing.android.integration
implementation libs.time.duration.picker
implementation libs.textdrawable
implementation libs.google.zxing.core
implementation (libs.subsampling.scale.image.view) {
exclude group: 'com.android.support', module: 'support-annotations'
@@ -495,6 +507,7 @@ dependencies {
implementation libs.lottie
implementation libs.stickyheadergrid
implementation libs.circular.progress.button
implementation libs.signal.android.database.sqlcipher
implementation libs.androidx.sqlite
@@ -505,33 +518,28 @@ dependencies {
}
implementation libs.dnsjava
spinnerImplementation project(":spinner")
spinnerImplementation libs.square.leakcanary
flipperImplementation libs.facebook.flipper
flipperImplementation libs.facebook.soloader
testImplementation testLibs.junit.junit
testImplementation testLibs.assertj.core
testImplementation testLibs.mockito.core
testImplementation testLibs.mockito.kotlin
testImplementation testLibs.powermock.api.mockito
testImplementation testLibs.powermock.module.junit4.core
testImplementation testLibs.powermock.module.junit4.rule
testImplementation testLibs.powermock.classloading.xstream
testImplementation testLibs.androidx.test.core
testImplementation (testLibs.robolectric.robolectric) {
exclude group: 'com.google.protobuf', module: 'protobuf-java'
}
testImplementation testLibs.robolectric.shadows.multidex
testImplementation (testLibs.bouncycastle.bcprov.jdk15on) {
force = true
}
testImplementation testLibs.hamcrest.hamcrest
testImplementation(testFixtures(project(":libsignal-service")))
androidTestImplementation testLibs.androidx.test.ext.junit
androidTestImplementation testLibs.espresso.core
androidTestImplementation testLibs.androidx.test.core
androidTestImplementation testLibs.androidx.test.core.ktx
androidTestImplementation testLibs.androidx.test.ext.junit.ktx
testImplementation testLibs.espresso.core
implementation libs.kotlin.stdlib.jdk8
implementation libs.kotlin.reflect
@@ -539,9 +547,10 @@ dependencies {
implementation libs.rxjava3.rxandroid
implementation libs.rxjava3.rxkotlin
implementation libs.rxdogtag
}
androidTestUtil 'androidx.test:orchestrator:1.4.1'
dependencyVerification {
configuration = '(play|website)(Prod|Staging)(Debug|Release)RuntimeClasspath'
}
def getLastCommitTimestamp() {
@@ -587,8 +596,7 @@ def getCurrentGitTag() {
def output = stdout.toString().trim()
if (output != null && output.size() > 0) {
def tags = output.split('\n').toList()
return tags.stream().filter(t -> t.contains('nightly')).findFirst().orElse(tags.get(0))
return output.split('\n')[0];
} else {
return null
}

View File

@@ -41,5 +41,4 @@
<ignore path="*/org/thoughtcrime/securesms/jobs/StickerPackDownloadJob.java" />
</issue>
<issue id="OptionalUsedAsFieldOrParameterType" severity="ignore" />
</lint>

View File

@@ -2,7 +2,6 @@
-dontobfuscate
-keepattributes SourceFile,LineNumberTable
-keep class org.whispersystems.** { *; }
-keep class org.signal.libsignal.protocol.** { *; }
-keep class org.thoughtcrime.securesms.** { *; }
-keepclassmembers class ** {
public void onEvent*(**);

View File

@@ -1,66 +0,0 @@
package org.thoughtcrime.securesms.conversation
import androidx.test.core.app.ActivityScenario
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey
import org.thoughtcrime.securesms.database.IdentityDatabase
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.profiles.ProfileName
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.safety.SafetyNumberBottomSheet
import org.thoughtcrime.securesms.testing.SignalActivityRule
/**
* Android test to help show SNC dialog quickly with custom data to make sure it displays properly.
*/
@RunWith(AndroidJUnit4::class)
class SafetyNumberChangeDialogPreviewer {
@get:Rule val harness = SignalActivityRule(othersCount = 10)
@Test
fun testShowLongName() {
val other: Recipient = Recipient.resolved(harness.others.first())
SignalDatabase.recipients.setProfileName(other.id, ProfileName.fromParts("Super really long name like omg", "But seriously it's long like really really long"))
harness.setVerified(other, IdentityDatabase.VerifiedStatus.VERIFIED)
harness.changeIdentityKey(other)
val scenario: ActivityScenario<ConversationActivity> = harness.launchActivity { putExtra("recipient_id", other.id.serialize()) }
scenario.onActivity {
SafetyNumberBottomSheet.forRecipientId(other.id).show(it.supportFragmentManager)
}
// Uncomment to make dialog stay on screen, otherwise will show/dismiss immediately
// ThreadUtil.sleep(15000)
}
@Test
fun testShowLargeSheet() {
val othersRecipients = harness.others.map { Recipient.resolved(it) }
othersRecipients.forEach { other ->
SignalDatabase.recipients.setProfileName(other.id, ProfileName.fromParts("My", "Name"))
harness.setVerified(other, IdentityDatabase.VerifiedStatus.DEFAULT)
harness.changeIdentityKey(other)
}
val scenario: ActivityScenario<ConversationActivity> = harness.launchActivity { putExtra("recipient_id", harness.others.first().serialize()) }
scenario.onActivity { conversationActivity ->
SafetyNumberBottomSheet
.forIdentityRecordsAndDestinations(
identityRecords = ApplicationDependencies.getProtocolStore().aci().identities().getIdentityRecords(othersRecipients).identityRecords,
destinations = othersRecipients.map { ContactSearchKey.RecipientSearchKey.KnownRecipient(it.id) }
)
.show(conversationActivity.supportFragmentManager)
}
// Uncomment to make dialog stay on screen, otherwise will show/dismiss immediately
// ThreadUtil.sleep(15000)
}
}

View File

@@ -1,157 +0,0 @@
package org.thoughtcrime.securesms.database
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
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.ApplicationDependencies
import java.util.concurrent.CountDownLatch
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger
/**
* When writing tests, be very careful to call [DatabaseObserver.flush] before asserting any observer state. Internally, the observer is enqueueing tasks on
* an executor, and failing to flush the executor will lead to incorrect/flaky tests.
*/
@RunWith(AndroidJUnit4::class)
class DatabaseObserverTest {
private lateinit var db: SQLiteDatabase
private lateinit var observer: DatabaseObserver
@Before
fun setup() {
db = SignalDatabase.instance!!.signalWritableDatabase
observer = ApplicationDependencies.getDatabaseObserver()
}
@Test
fun notifyConversationListeners_runsImmediatelyIfNotInTransaction() {
val hasRun = AtomicBoolean(false)
observer.registerConversationObserver(1) { hasRun.set(true) }
observer.notifyConversationListeners(1)
observer.flush()
assertTrue(hasRun.get())
}
@Test
fun notifyConversationListeners_runsAfterSuccessIfInTransaction() {
val hasRun = AtomicBoolean(false)
db.beginTransaction()
observer.registerConversationObserver(1) { hasRun.set(true) }
observer.notifyConversationListeners(1)
observer.flush()
assertFalse(hasRun.get())
db.setTransactionSuccessful()
db.endTransaction()
observer.flush()
assertTrue(hasRun.get())
}
@Test
fun notifyConversationListeners_doesNotRunAfterFailedTransaction() {
val hasRun = AtomicBoolean(false)
db.beginTransaction()
observer.registerConversationObserver(1) { hasRun.set(true) }
observer.notifyConversationListeners(1)
observer.flush()
assertFalse(hasRun.get())
db.endTransaction()
observer.flush()
assertFalse(hasRun.get())
// Verifying we still don't run it even after a subsequent success
db.beginTransaction()
db.setTransactionSuccessful()
db.endTransaction()
observer.flush()
assertFalse(hasRun.get())
}
@Test
fun notifyConversationListeners_onlyRunAfterAllTransactionsComplete() {
val hasRun = AtomicBoolean(false)
db.beginTransaction()
observer.registerConversationObserver(1) { hasRun.set(true) }
observer.notifyConversationListeners(1)
observer.flush()
assertFalse(hasRun.get())
db.beginTransaction()
db.setTransactionSuccessful()
db.endTransaction()
observer.flush()
assertFalse(hasRun.get())
db.setTransactionSuccessful()
db.endTransaction()
observer.flush()
assertTrue(hasRun.get())
}
@Test
fun notifyConversationListeners_runsImmediatelyIfTheTransactionIsOnAnotherThread() {
db.beginTransaction()
val latch = CountDownLatch(1)
SignalExecutors.BOUNDED.execute {
val hasRun = AtomicBoolean(false)
observer.registerConversationObserver(1) { hasRun.set(true) }
observer.notifyConversationListeners(1)
observer.flush()
assertTrue(hasRun.get())
latch.countDown()
}
latch.await()
db.setTransactionSuccessful()
db.endTransaction()
}
@Test
fun notifyConversationListeners_runsAfterSuccessIfInTransaction_ignoreDuplicateNotifications() {
val thread1Count = AtomicInteger(0)
val thread2Count = AtomicInteger(0)
db.beginTransaction()
observer.registerConversationObserver(1) { thread1Count.incrementAndGet() }
observer.registerConversationObserver(2) { thread2Count.incrementAndGet() }
observer.notifyConversationListeners(1)
observer.notifyConversationListeners(2)
observer.notifyConversationListeners(2)
observer.flush()
assertEquals(0, thread1Count.get())
assertEquals(0, thread2Count.get())
db.setTransactionSuccessful()
db.endTransaction()
observer.flush()
assertEquals(1, thread1Count.get())
assertEquals(1, thread2Count.get())
}
}

View File

@@ -1,98 +0,0 @@
package org.thoughtcrime.securesms.database
import org.junit.Assert
import org.junit.Before
import org.junit.Test
import org.thoughtcrime.securesms.database.model.DistributionListId
import org.thoughtcrime.securesms.database.model.DistributionListRecord
import org.thoughtcrime.securesms.database.model.StoryType
import org.thoughtcrime.securesms.recipients.RecipientId
import org.whispersystems.signalservice.api.push.ACI
import java.util.UUID
class DistributionListDatabaseTest {
private lateinit var distributionDatabase: DistributionListDatabase
@Before
fun setup() {
distributionDatabase = SignalDatabase.distributionLists
}
@Test
fun createList_whenNoConflict_insertSuccessfully() {
val id: DistributionListId? = distributionDatabase.createList("test", recipientList(1, 2, 3))
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)
val members: List<RecipientId> = recipientList(1, 2, 3)
val id: DistributionListId? = distributionDatabase.createList("test", members)
Assert.assertNotNull(id)
val record: DistributionListRecord? = distributionDatabase.getList(id!!)
Assert.assertNotNull(record)
Assert.assertEquals(id, record!!.id)
Assert.assertEquals("test", record.name)
Assert.assertEquals(members, record.members)
}
@Test
fun getMembers_returnsCorrectMembers() {
createRecipients(3)
val members: List<RecipientId> = recipientList(1, 2, 3)
val id: DistributionListId? = distributionDatabase.createList("test", members)
Assert.assertNotNull(id)
val foundMembers: List<RecipientId> = distributionDatabase.getMembers(id!!)
Assert.assertEquals(members, foundMembers)
}
@Test
fun givenStoryExists_getStoryType_returnsStoryWithReplies() {
val id: DistributionListId? = distributionDatabase.createList("test", recipientList(1, 2, 3))
Assert.assertNotNull(id)
val storyType = distributionDatabase.getStoryType(id!!)
Assert.assertEquals(StoryType.STORY_WITH_REPLIES, storyType)
}
@Test
fun givenStoryExistsAndMarkedNoReplies_getStoryType_returnsStoryWithoutReplies() {
val id: DistributionListId? = distributionDatabase.createList("test", recipientList(1, 2, 3))
Assert.assertNotNull(id)
distributionDatabase.setAllowsReplies(id!!, false)
val storyType = distributionDatabase.getStoryType(id)
Assert.assertEquals(StoryType.STORY_WITHOUT_REPLIES, storyType)
}
@Test(expected = IllegalStateException::class)
fun givenStoryDoesNotExist_getStoryType_throwsIllegalStateException() {
distributionDatabase.getStoryType(DistributionListId.from(12))
Assert.fail("Expected an assertion error.")
}
private fun createRecipients(count: Int) {
for (i in 0 until count) {
SignalDatabase.recipients.getOrInsertFromServiceId(ACI.from(UUID.randomUUID()))
}
}
private fun recipientList(vararg ids: Long): List<RecipientId> {
return ids.map { RecipientId.from(it) }
}
}

View File

@@ -1,188 +0,0 @@
package org.thoughtcrime.securesms.database
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.thoughtcrime.securesms.database.model.databaseprotos.GiftBadge
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.whispersystems.signalservice.api.push.ACI
import org.whispersystems.signalservice.api.push.PNI
import org.whispersystems.signalservice.api.push.ServiceId
import java.util.UUID
@Suppress("ClassName")
@RunWith(AndroidJUnit4::class)
class MmsDatabaseTest_gifts {
private lateinit var mms: MmsDatabase
private val localAci = ACI.from(UUID.randomUUID())
private val localPni = PNI.from(UUID.randomUUID())
private lateinit var recipients: List<RecipientId>
@Before
fun setUp() {
mms = SignalDatabase.mms
mms.deleteAllThreads()
SignalStore.account().setAci(localAci)
SignalStore.account().setPni(localPni)
recipients = (0 until 5).map { SignalDatabase.recipients.getOrInsertFromServiceId(ServiceId.from(UUID.randomUUID())) }
}
@Test
fun givenNoSentGifts_whenISetOutgoingGiftsRevealed_thenIExpectEmptyList() {
val result = mms.setOutgoingGiftsRevealed(listOf(1))
assertTrue(result.isEmpty())
}
@Test
fun givenSentGift_whenISetOutgoingGiftsRevealed_thenIExpectNonEmptyListContainingThatGift() {
val messageId = MmsHelper.insert(
recipient = Recipient.resolved(recipients[0]),
sentTimeMillis = 1,
giftBadge = GiftBadge.getDefaultInstance()
)
val result = mms.setOutgoingGiftsRevealed(listOf(messageId))
assertTrue(result.isNotEmpty())
assertEquals(messageId, result.first().messageId.id)
}
@Test
fun givenViewedSentGift_whenISetOutgoingGiftsRevealed_thenIExpectEmptyList() {
val messageId = MmsHelper.insert(
recipient = Recipient.resolved(recipients[0]),
sentTimeMillis = 1,
giftBadge = GiftBadge.getDefaultInstance()
)
mms.setOutgoingGiftsRevealed(listOf(messageId))
val result = mms.setOutgoingGiftsRevealed(listOf(messageId))
assertTrue(result.isEmpty())
}
@Test
fun givenMultipleSentGift_whenISetOutgoingGiftsRevealedForOne_thenIExpectNonEmptyListContainingThatGift() {
val messageId = MmsHelper.insert(
recipient = Recipient.resolved(recipients[0]),
sentTimeMillis = 1,
giftBadge = GiftBadge.getDefaultInstance()
)
MmsHelper.insert(
recipient = Recipient.resolved(recipients[0]),
sentTimeMillis = 1,
giftBadge = GiftBadge.getDefaultInstance()
)
val result = mms.setOutgoingGiftsRevealed(listOf(messageId))
assertEquals(1, result.size)
assertEquals(messageId, result.first().messageId.id)
}
@Test
fun givenMultipleSentGift_whenISetOutgoingGiftsRevealedForBoth_thenIExpectNonEmptyListContainingThoseGifts() {
val messageId = MmsHelper.insert(
recipient = Recipient.resolved(recipients[0]),
sentTimeMillis = 1,
giftBadge = GiftBadge.getDefaultInstance()
)
val messageId2 = MmsHelper.insert(
recipient = Recipient.resolved(recipients[0]),
sentTimeMillis = 1,
giftBadge = GiftBadge.getDefaultInstance()
)
val result = mms.setOutgoingGiftsRevealed(listOf(messageId, messageId2))
assertEquals(listOf(messageId, messageId2), result.map { it.messageId.id })
}
@Test
fun givenMultipleSentGiftAndNonGift_whenISetOutgoingGiftsRevealedForBothGifts_thenIExpectNonEmptyListContainingJustThoseGifts() {
val messageId = MmsHelper.insert(
recipient = Recipient.resolved(recipients[0]),
sentTimeMillis = 1,
giftBadge = GiftBadge.getDefaultInstance()
)
val messageId2 = MmsHelper.insert(
recipient = Recipient.resolved(recipients[0]),
sentTimeMillis = 1,
giftBadge = GiftBadge.getDefaultInstance()
)
MmsHelper.insert(
recipient = Recipient.resolved(recipients[0]),
sentTimeMillis = 1,
giftBadge = null
)
val result = mms.setOutgoingGiftsRevealed(listOf(messageId, messageId2))
assertEquals(listOf(messageId, messageId2), result.map { it.messageId.id })
}
@Test
fun givenMultipleSentGiftAndNonGift_whenISetOutgoingGiftsRevealedForAllThree_thenIExpectNonEmptyListContainingJustThoseGifts() {
val messageId = MmsHelper.insert(
recipient = Recipient.resolved(recipients[0]),
sentTimeMillis = 1,
giftBadge = GiftBadge.getDefaultInstance()
)
val messageId2 = MmsHelper.insert(
recipient = Recipient.resolved(recipients[0]),
sentTimeMillis = 1,
giftBadge = GiftBadge.getDefaultInstance()
)
val messageId3 = MmsHelper.insert(
recipient = Recipient.resolved(recipients[0]),
sentTimeMillis = 1,
giftBadge = null
)
val result = mms.setOutgoingGiftsRevealed(listOf(messageId, messageId2, messageId3))
assertEquals(listOf(messageId, messageId2), result.map { it.messageId.id })
}
@Test
fun givenMultipleSentGiftAndNonGift_whenISetOutgoingGiftsRevealedForNonGift_thenIExpectEmptyList() {
MmsHelper.insert(
recipient = Recipient.resolved(recipients[0]),
sentTimeMillis = 1,
giftBadge = GiftBadge.getDefaultInstance()
)
MmsHelper.insert(
recipient = Recipient.resolved(recipients[0]),
sentTimeMillis = 1,
giftBadge = GiftBadge.getDefaultInstance()
)
val messageId3 = MmsHelper.insert(
recipient = Recipient.resolved(recipients[0]),
sentTimeMillis = 1,
giftBadge = null
)
val result = mms.setOutgoingGiftsRevealed(listOf(messageId3))
assertTrue(result.isEmpty())
}
}

View File

@@ -1,336 +0,0 @@
package org.thoughtcrime.securesms.database
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.thoughtcrime.securesms.database.model.DistributionListId
import org.thoughtcrime.securesms.database.model.ParentStoryId
import org.thoughtcrime.securesms.database.model.StoryType
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.mms.IncomingMediaMessage
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.whispersystems.signalservice.api.push.ACI
import org.whispersystems.signalservice.api.push.PNI
import org.whispersystems.signalservice.api.push.ServiceId
import java.util.UUID
@Suppress("ClassName")
@RunWith(AndroidJUnit4::class)
class MmsDatabaseTest_stories {
private lateinit var mms: MmsDatabase
private val localAci = ACI.from(UUID.randomUUID())
private val localPni = PNI.from(UUID.randomUUID())
private lateinit var myStory: Recipient
private lateinit var recipients: List<RecipientId>
@Before
fun setUp() {
mms = SignalDatabase.mms
mms.deleteAllThreads()
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(ServiceId.from(UUID.randomUUID())) }
}
@Test
fun givenNoStories_whenIGetOrderedStoryRecipientsAndIds_thenIExpectAnEmptyList() {
// WHEN
val result = mms.orderedStoryRecipientsAndIds
// THEN
assertEquals(0, result.size)
}
@Test
fun givenOneOutgoingAndOneIncomingStory_whenIGetOrderedStoryRecipientsAndIds_thenIExpectIncomingThenOutgoing() {
// GIVEN
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(myStory)
val sender = recipients[0]
MmsHelper.insert(
recipient = myStory,
sentTimeMillis = 1,
storyType = StoryType.STORY_WITH_REPLIES,
threadId = threadId
)
MmsHelper.insert(
IncomingMediaMessage(
from = sender,
sentTimeMillis = 2,
serverTimeMillis = 2,
receivedTimeMillis = 2,
storyType = StoryType.STORY_WITH_REPLIES
),
-1L
)
// WHEN
val result = mms.orderedStoryRecipientsAndIds
// THEN
assertEquals(listOf(sender.toLong(), myStory.id.toLong()), result.map { it.recipientId.toLong() })
}
@Test
fun givenAStory_whenISetIncomingStoryMessageViewed_thenIExpectASetReceiptTimestamp() {
// GIVEN
val sender = recipients[0]
val messageId = MmsHelper.insert(
IncomingMediaMessage(
from = sender,
sentTimeMillis = 2,
serverTimeMillis = 2,
receivedTimeMillis = 2,
storyType = StoryType.STORY_WITH_REPLIES
),
-1L
).get().messageId
val messageBeforeMark = SignalDatabase.mms.getMessageRecord(messageId)
assertFalse(messageBeforeMark.incomingStoryViewedAtTimestamp > 0)
// WHEN
SignalDatabase.mms.setIncomingMessageViewed(messageId)
// THEN
val messageAfterMark = SignalDatabase.mms.getMessageRecord(messageId)
assertTrue(messageAfterMark.incomingStoryViewedAtTimestamp > 0)
}
@Test
fun given5ViewedStories_whenIGetOrderedStoryRecipientsAndIds_thenIExpectLatestViewedFirst() {
// GIVEN
val messageIds = recipients.take(5).map {
MmsHelper.insert(
IncomingMediaMessage(
from = it,
sentTimeMillis = 2,
serverTimeMillis = 2,
receivedTimeMillis = 2,
storyType = StoryType.STORY_WITH_REPLIES,
),
-1L
).get().messageId
}
val randomizedOrderedIds = messageIds.shuffled()
randomizedOrderedIds.forEach {
SignalDatabase.mms.setIncomingMessageViewed(it)
Thread.sleep(5)
}
// WHEN
val result = SignalDatabase.mms.orderedStoryRecipientsAndIds
val resultOrderedIds = result.map { it.messageId }
// THEN
assertEquals(randomizedOrderedIds.reversed(), resultOrderedIds)
}
@Test
fun given15Stories_whenIGetOrderedStoryRecipientsAndIds_thenIExpectUnviewedThenInterspersedViewedAndSelfSendsAllDescending() {
val myStoryThread = SignalDatabase.threads.getOrCreateThreadIdFor(myStory)
val unviewedIds: List<Long> = (0 until 5).map {
Thread.sleep(5)
MmsHelper.insert(
IncomingMediaMessage(
from = recipients[it],
sentTimeMillis = System.currentTimeMillis(),
serverTimeMillis = 2,
receivedTimeMillis = 2,
storyType = StoryType.STORY_WITH_REPLIES,
),
-1L
).get().messageId
}
val viewedIds: List<Long> = (0 until 5).map {
Thread.sleep(5)
MmsHelper.insert(
IncomingMediaMessage(
from = recipients[it],
sentTimeMillis = System.currentTimeMillis(),
serverTimeMillis = 2,
receivedTimeMillis = 2,
storyType = StoryType.STORY_WITH_REPLIES,
),
-1L
).get().messageId
}
val interspersedIds: List<Long> = (0 until 10).map {
Thread.sleep(5)
if (it % 2 == 0) {
SignalDatabase.mms.setIncomingMessageViewed(viewedIds[it / 2])
viewedIds[it / 2]
} else {
MmsHelper.insert(
recipient = myStory,
sentTimeMillis = System.currentTimeMillis(),
storyType = StoryType.STORY_WITH_REPLIES,
threadId = myStoryThread
)
}
}
val result = SignalDatabase.mms.orderedStoryRecipientsAndIds
val resultOrderedIds = result.map { it.messageId }
assertEquals(unviewedIds.reversed() + interspersedIds.reversed(), resultOrderedIds)
}
@Test
fun givenNoStories_whenICheckIsOutgoingStoryAlreadyInDatabase_thenIExpectFalse() {
// WHEN
val result = mms.isOutgoingStoryAlreadyInDatabase(recipients[0], 200)
// THEN
assertFalse(result)
}
@Test
fun givenNoOutgoingStories_whenICheckIsOutgoingStoryAlreadyInDatabase_thenIExpectFalse() {
// GIVEN
MmsHelper.insert(
IncomingMediaMessage(
from = recipients[0],
sentTimeMillis = 200,
serverTimeMillis = 2,
receivedTimeMillis = 2,
storyType = StoryType.STORY_WITH_REPLIES,
),
-1L
)
// WHEN
val result = mms.isOutgoingStoryAlreadyInDatabase(recipients[0], 200)
// THEN
assertFalse(result)
}
@Test
fun givenOutgoingStoryExistsForRecipientAndTime_whenICheckIsOutgoingStoryAlreadyInDatabase_thenIExpectTrue() {
// GIVEN
MmsHelper.insert(
recipient = myStory,
sentTimeMillis = 200,
storyType = StoryType.STORY_WITH_REPLIES,
threadId = -1L
)
// WHEN
val result = mms.isOutgoingStoryAlreadyInDatabase(myStory.id, 200)
// THEN
assertTrue(result)
}
@Test
fun givenAGroupStoryWithNoReplies_whenICheckHasSelfReplyInGroupStory_thenIExpectFalse() {
// GIVEN
val groupStoryId = MmsHelper.insert(
recipient = myStory,
sentTimeMillis = 200,
storyType = StoryType.STORY_WITH_REPLIES,
threadId = -1L
)
// WHEN
val result = mms.hasSelfReplyInGroupStory(groupStoryId)
// THEN
assertFalse(result)
}
@Test
fun givenAGroupStoryWithAReplyFromSelf_whenICheckHasSelfReplyInGroupStory_thenIExpectTrue() {
// GIVEN
val groupStoryId = MmsHelper.insert(
recipient = myStory,
sentTimeMillis = 200,
storyType = StoryType.STORY_WITH_REPLIES,
threadId = -1L
)
MmsHelper.insert(
recipient = myStory,
sentTimeMillis = 201,
storyType = StoryType.NONE,
parentStoryId = ParentStoryId.GroupReply(groupStoryId)
)
// WHEN
val result = mms.hasSelfReplyInGroupStory(groupStoryId)
// THEN
assertTrue(result)
}
@Test
fun givenAGroupStoryWithAReactionFromSelf_whenICheckHasSelfReplyInGroupStory_thenIExpectFalse() {
// GIVEN
val groupStoryId = MmsHelper.insert(
recipient = myStory,
sentTimeMillis = 200,
storyType = StoryType.STORY_WITH_REPLIES,
threadId = -1L
)
MmsHelper.insert(
recipient = myStory,
sentTimeMillis = 201,
storyType = StoryType.NONE,
parentStoryId = ParentStoryId.GroupReply(groupStoryId),
isStoryReaction = true
)
// WHEN
val result = mms.hasSelfReplyInGroupStory(groupStoryId)
// THEN
assertFalse(result)
}
@Test
fun givenAGroupStoryWithAReplyFromSomeoneElse_whenICheckHasSelfReplyInGroupStory_thenIExpectFalse() {
// GIVEN
val groupStoryId = MmsHelper.insert(
recipient = myStory,
sentTimeMillis = 200,
storyType = StoryType.STORY_WITH_REPLIES,
threadId = -1L
)
MmsHelper.insert(
IncomingMediaMessage(
from = myStory.id,
sentTimeMillis = 201,
serverTimeMillis = 201,
receivedTimeMillis = 202,
parentStoryId = ParentStoryId.GroupReply(groupStoryId)
),
-1
)
// WHEN
val result = mms.hasSelfReplyInGroupStory(groupStoryId)
// THEN
assertFalse(result)
}
}

View File

@@ -1,70 +0,0 @@
package org.thoughtcrime.securesms.database
import org.thoughtcrime.securesms.database.model.ParentStoryId
import org.thoughtcrime.securesms.database.model.StoryType
import org.thoughtcrime.securesms.database.model.databaseprotos.GiftBadge
import org.thoughtcrime.securesms.mms.IncomingMediaMessage
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage
import org.thoughtcrime.securesms.recipients.Recipient
import java.util.Optional
/**
* Helper methods for inserting an MMS message into the MMS table.
*/
object MmsHelper {
fun insert(
recipient: Recipient = Recipient.UNKNOWN,
body: String = "body",
sentTimeMillis: Long = System.currentTimeMillis(),
subscriptionId: Int = -1,
expiresIn: Long = 0,
viewOnce: Boolean = false,
distributionType: Int = ThreadDatabase.DistributionTypes.DEFAULT,
threadId: Long = 1,
storyType: StoryType = StoryType.NONE,
parentStoryId: ParentStoryId? = null,
isStoryReaction: Boolean = false,
giftBadge: GiftBadge? = null
): Long {
val message = OutgoingMediaMessage(
recipient,
body,
emptyList(),
sentTimeMillis,
subscriptionId,
expiresIn,
viewOnce,
distributionType,
storyType,
parentStoryId,
isStoryReaction,
null,
emptyList(),
emptyList(),
emptyList(),
emptySet(),
emptySet(),
giftBadge
)
return insert(
message = message,
threadId = threadId
)
}
fun insert(
message: OutgoingMediaMessage,
threadId: Long
): Long {
return SignalDatabase.mms.insertMessageOutbox(message, threadId, false, GroupReceiptDatabase.STATUS_UNKNOWN, null)
}
fun insert(
message: IncomingMediaMessage,
threadId: Long
): Optional<MessageDatabase.InsertResult> {
return SignalDatabase.mms.insertSecureDecryptedMessageInbox(message, threadId)
}
}

View File

@@ -1,660 +0,0 @@
package org.thoughtcrime.securesms.database
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.hamcrest.MatcherAssert
import org.hamcrest.Matchers
import org.junit.Assert
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.Test
import org.junit.runner.RunWith
import org.signal.core.util.CursorUtil
import org.signal.core.util.ThreadUtil
import org.signal.libsignal.protocol.IdentityKey
import org.signal.libsignal.protocol.SignalProtocolAddress
import org.signal.libsignal.protocol.state.SessionRecord
import org.signal.libsignal.zkgroup.groups.GroupMasterKey
import org.signal.storageservice.protos.groups.local.DecryptedGroup
import org.signal.storageservice.protos.groups.local.DecryptedMember
import org.thoughtcrime.securesms.conversation.colors.AvatarColor
import org.thoughtcrime.securesms.database.model.DistributionListId
import org.thoughtcrime.securesms.database.model.DistributionListRecord
import org.thoughtcrime.securesms.database.model.Mention
import org.thoughtcrime.securesms.database.model.MessageId
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.database.model.ReactionRecord
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.jobs.RecipientChangedNumberJob
import org.thoughtcrime.securesms.keyvalue.AccountValues
import org.thoughtcrime.securesms.keyvalue.KeyValueDataSet
import org.thoughtcrime.securesms.keyvalue.KeyValueStore
import org.thoughtcrime.securesms.keyvalue.MockKeyValuePersistentStorage
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.mms.IncomingMediaMessage
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.sms.IncomingTextMessage
import org.whispersystems.signalservice.api.push.ACI
import org.whispersystems.signalservice.api.push.PNI
import org.whispersystems.signalservice.api.util.UuidUtil
import java.util.Optional
import java.util.UUID
@RunWith(AndroidJUnit4::class)
class RecipientDatabaseTest_getAndPossiblyMergeLegacy {
private lateinit var recipientDatabase: RecipientDatabase
private lateinit var identityDatabase: IdentityDatabase
private lateinit var groupReceiptDatabase: GroupReceiptDatabase
private lateinit var groupDatabase: GroupDatabase
private lateinit var threadDatabase: ThreadDatabase
private lateinit var smsDatabase: MessageDatabase
private lateinit var mmsDatabase: MessageDatabase
private lateinit var sessionDatabase: SessionDatabase
private lateinit var mentionDatabase: MentionDatabase
private lateinit var reactionDatabase: ReactionDatabase
private lateinit var notificationProfileDatabase: NotificationProfileDatabase
private lateinit var distributionListDatabase: DistributionListDatabase
private val localAci = ACI.from(UUID.randomUUID())
private val localPni = PNI.from(UUID.randomUUID())
@Before
fun setup() {
recipientDatabase = SignalDatabase.recipients
recipientDatabase = SignalDatabase.recipients
identityDatabase = SignalDatabase.identities
groupReceiptDatabase = SignalDatabase.groupReceipts
groupDatabase = SignalDatabase.groups
threadDatabase = SignalDatabase.threads
smsDatabase = SignalDatabase.sms
mmsDatabase = SignalDatabase.mms
sessionDatabase = SignalDatabase.sessions
mentionDatabase = SignalDatabase.mentions
reactionDatabase = SignalDatabase.reactions
notificationProfileDatabase = SignalDatabase.notificationProfiles
distributionListDatabase = SignalDatabase.distributionLists
ensureDbEmpty()
SignalStore.account().setAci(localAci)
SignalStore.account().setPni(localPni)
}
// ==============================================================
// If both the ACI and E164 map to no one
// ==============================================================
/** If all you have is an ACI, you can just store that, regardless of trust level. */
@Test
fun getAndPossiblyMerge_aciAndE164MapToNoOne_aciOnly() {
val recipientId: RecipientId = recipientDatabase.getAndPossiblyMergeLegacy(ACI_A, null)
val recipient = Recipient.resolved(recipientId)
assertEquals(ACI_A, recipient.requireServiceId())
assertFalse(recipient.hasE164())
}
/** If all you have is an E164, you can just store that, regardless of trust level. */
@Test
fun getAndPossiblyMerge_aciAndE164MapToNoOne_e164Only() {
val recipientId: RecipientId = recipientDatabase.getAndPossiblyMergeLegacy(null, E164_A)
val recipient = Recipient.resolved(recipientId)
assertEquals(E164_A, recipient.requireE164())
assertFalse(recipient.hasServiceId())
}
/** With high trust, you can associate an ACI-e164 pair. */
@Test
fun getAndPossiblyMerge_aciAndE164MapToNoOne_aciAndE164() {
val recipientId: RecipientId = recipientDatabase.getAndPossiblyMergeLegacy(ACI_A, E164_A)
val recipient = Recipient.resolved(recipientId)
assertEquals(ACI_A, recipient.requireServiceId())
assertEquals(E164_A, recipient.requireE164())
}
// ==============================================================
// If the ACI maps to an existing user, but the E164 doesn't
// ==============================================================
/** You can associate an e164 with an existing ACI. */
@Test
fun getAndPossiblyMerge_aciMapsToExistingUserButE164DoesNot_aciAndE164() {
val existingId: RecipientId = recipientDatabase.getOrInsertFromServiceId(ACI_A)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMergeLegacy(ACI_A, E164_A)
assertEquals(existingId, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
assertEquals(E164_A, retrievedRecipient.requireE164())
}
/** Basically the change number case. Update the existing user. */
@Test
fun getAndPossiblyMerge_aciMapsToExistingUserButE164DoesNot_aciAndE164_2() {
val existingId: RecipientId = recipientDatabase.getAndPossiblyMergeLegacy(ACI_A, E164_A)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMergeLegacy(ACI_A, E164_B)
assertEquals(existingId, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
assertEquals(E164_B, retrievedRecipient.requireE164())
}
// ==============================================================
// If the E164 maps to an existing user, but the ACI doesn't
// ==============================================================
/** You can associate an e164 with an existing ACI. */
@Test
fun getAndPossiblyMerge_e164MapsToExistingUserButAciDoesNot_aciAndE164() {
val existingId: RecipientId = recipientDatabase.getOrInsertFromE164(E164_A)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMergeLegacy(ACI_A, E164_A)
assertEquals(existingId, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
assertEquals(E164_A, retrievedRecipient.requireE164())
}
/** We never change the ACI of an existing row. New ACI = new person. Take the e164 from the current holder. */
@Test
fun getAndPossiblyMerge_e164MapsToExistingUserButAciDoesNot_aciAndE164_2() {
val existingId: RecipientId = recipientDatabase.getAndPossiblyMergeLegacy(ACI_A, E164_A)
recipientDatabase.setPni(existingId, PNI_A)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMergeLegacy(ACI_B, E164_A)
recipientDatabase.setPni(retrievedId, PNI_A)
assertNotEquals(existingId, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_B, retrievedRecipient.requireServiceId())
assertEquals(E164_A, retrievedRecipient.requireE164())
val existingRecipient = Recipient.resolved(existingId)
assertEquals(ACI_A, existingRecipient.requireServiceId())
assertFalse(existingRecipient.hasE164())
}
/** We never want to remove the e164 of our own contact entry. Leave the e164 alone. */
@Test
fun getAndPossiblyMerge_e164MapsToExistingUserButAciDoesNot_e164BelongsToLocalUser() {
val dataSet = KeyValueDataSet().apply {
putString(AccountValues.KEY_E164, E164_A)
putString(AccountValues.KEY_ACI, ACI_A.toString())
}
SignalStore.inject(KeyValueStore(MockKeyValuePersistentStorage.withDataSet(dataSet)))
val existingId: RecipientId = recipientDatabase.getAndPossiblyMergeLegacy(ACI_A, E164_A)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMergeLegacy(ACI_B, E164_A)
assertNotEquals(existingId, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_B, retrievedRecipient.requireServiceId())
assertFalse(retrievedRecipient.hasE164())
val existingRecipient = Recipient.resolved(existingId)
assertEquals(ACI_A, existingRecipient.requireServiceId())
assertEquals(E164_A, existingRecipient.requireE164())
}
// ==============================================================
// If both the ACI and E164 map to an existing user
// ==============================================================
/** If your ACI and e164 match, youre good. */
@Test
fun getAndPossiblyMerge_bothAciAndE164MapToExistingUser_aciAndE164() {
val existingId: RecipientId = recipientDatabase.getAndPossiblyMergeLegacy(ACI_A, E164_A)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMergeLegacy(ACI_A, E164_A)
assertEquals(existingId, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
assertEquals(E164_A, retrievedRecipient.requireE164())
}
/** Merge two different users into one. You should prefer the ACI user. Not shown: merging threads, dropping e164 sessions, etc. */
@Test
fun getAndPossiblyMerge_bothAciAndE164MapToExistingUser_aciAndE164_merge() {
val changeNumberListener = ChangeNumberListener()
changeNumberListener.enqueue()
val existingAciId: RecipientId = recipientDatabase.getAndPossiblyMergeLegacy(ACI_A, null)
val existingE164Id: RecipientId = recipientDatabase.getAndPossiblyMergeLegacy(null, E164_A)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMergeLegacy(ACI_A, E164_A)
assertEquals(existingAciId, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
assertEquals(E164_A, retrievedRecipient.requireE164())
val existingE164Recipient = Recipient.resolved(existingE164Id)
assertEquals(retrievedId, existingE164Recipient.id)
changeNumberListener.waitForJobManager()
assertFalse(changeNumberListener.numberChangeWasEnqueued)
}
/** Same as [getAndPossiblyMerge_bothAciAndE164MapToExistingUser_aciAndE164_merge], but with a number change. */
@Test
fun getAndPossiblyMerge_bothAciAndE164MapToExistingUser_aciAndE164_merge_changedNumber() {
val changeNumberListener = ChangeNumberListener()
changeNumberListener.enqueue()
val existingAciId: RecipientId = recipientDatabase.getAndPossiblyMergeLegacy(ACI_A, E164_B)
val existingE164Id: RecipientId = recipientDatabase.getAndPossiblyMergeLegacy(null, E164_A)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMergeLegacy(ACI_A, E164_A)
assertEquals(existingAciId, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
assertEquals(E164_A, retrievedRecipient.requireE164())
val existingE164Recipient = Recipient.resolved(existingE164Id)
assertEquals(retrievedId, existingE164Recipient.id)
changeNumberListener.waitForJobManager()
assert(changeNumberListener.numberChangeWasEnqueued)
}
/** No new rules here, just a more complex scenario to show how different rules interact. */
@Test
fun getAndPossiblyMerge_bothAciAndE164MapToExistingUser_aciAndE164_complex() {
val changeNumberListener = ChangeNumberListener()
changeNumberListener.enqueue()
val existingId1: RecipientId = recipientDatabase.getAndPossiblyMergeLegacy(ACI_A, E164_B)
val existingId2: RecipientId = recipientDatabase.getAndPossiblyMergeLegacy(ACI_B, E164_A)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMergeLegacy(ACI_A, E164_A)
assertEquals(existingId1, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
assertEquals(E164_A, retrievedRecipient.requireE164())
val existingRecipient2 = Recipient.resolved(existingId2)
assertEquals(ACI_B, existingRecipient2.requireServiceId())
assertFalse(existingRecipient2.hasE164())
assert(changeNumberListener.numberChangeWasEnqueued)
}
/**
* Another case that results in a merge. Nothing strictly new here, but this case is called out because its a merge but *also* an E164 change,
* which clients may need to know for UX purposes.
*/
@Test
fun getAndPossiblyMerge_bothAciAndE164MapToExistingUser_aciAndE164_mergeAndPhoneNumberChange() {
val existingId1: RecipientId = recipientDatabase.getAndPossiblyMergeLegacy(ACI_A, E164_B)
val existingId2: RecipientId = recipientDatabase.getAndPossiblyMergeLegacy(null, E164_A)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMergeLegacy(ACI_A, E164_A)
assertEquals(existingId1, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
assertEquals(E164_A, retrievedRecipient.requireE164())
assertFalse(recipientDatabase.getByE164(E164_B).isPresent)
val recipientWithId2 = Recipient.resolved(existingId2)
assertEquals(retrievedId, recipientWithId2.id)
}
/** We never want to remove the e164 of our own contact entry. Leave the e164 alone. */
@Test
fun getAndPossiblyMerge_bothAciAndE164MapToExistingUser_e164BelongsToLocalUser() {
val dataSet = KeyValueDataSet().apply {
putString(AccountValues.KEY_E164, E164_A)
putString(AccountValues.KEY_ACI, ACI_B.toString())
}
SignalStore.inject(KeyValueStore(MockKeyValuePersistentStorage.withDataSet(dataSet)))
val existingId1: RecipientId = recipientDatabase.getAndPossiblyMergeLegacy(ACI_B, E164_A)
val existingId2: RecipientId = recipientDatabase.getAndPossiblyMergeLegacy(ACI_A, null)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMergeLegacy(ACI_A, E164_A)
assertEquals(existingId2, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
assertFalse(retrievedRecipient.hasE164())
val recipientWithId1 = Recipient.resolved(existingId1)
assertEquals(ACI_B, recipientWithId1.requireServiceId())
assertEquals(E164_A, recipientWithId1.requireE164())
}
/** This is a case where normally we'd update the E164 of a user, but here the changeSelf flag is disabled, so we shouldn't. */
@Test
fun getAndPossiblyMerge_aciMapsToExistingUserButE164DoesNot_aciBelongsToLocalUser_changeSelfFalse() {
val dataSet = KeyValueDataSet().apply {
putString(AccountValues.KEY_E164, E164_A)
putString(AccountValues.KEY_ACI, ACI_A.toString())
}
SignalStore.inject(KeyValueStore(MockKeyValuePersistentStorage.withDataSet(dataSet)))
val existingId: RecipientId = recipientDatabase.getAndPossiblyMergeLegacy(ACI_A, E164_A)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMergeLegacy(ACI_A, E164_B, changeSelf = false)
assertEquals(existingId, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
assertEquals(E164_A, retrievedRecipient.requireE164())
}
/** This is a case where we're changing our own number, and it's allowed because changeSelf = true. */
@Test
fun getAndPossiblyMerge_aciMapsToExistingUserButE164DoesNot_aciBelongsToLocalUser_changeSelfTrue() {
val dataSet = KeyValueDataSet().apply {
putString(AccountValues.KEY_E164, E164_A)
putString(AccountValues.KEY_ACI, ACI_A.toString())
}
SignalStore.inject(KeyValueStore(MockKeyValuePersistentStorage.withDataSet(dataSet)))
val existingId: RecipientId = recipientDatabase.getAndPossiblyMergeLegacy(ACI_A, E164_A)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMergeLegacy(ACI_A, E164_B, changeSelf = true)
assertEquals(existingId, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
assertEquals(E164_B, retrievedRecipient.requireE164())
}
/** Verifying a case where a change number job is expected to be enqueued. */
@Test
fun getAndPossiblyMerge_aciMapsToExistingUserButE164DoesNot_changedNumber() {
val changeNumberListener = ChangeNumberListener()
changeNumberListener.enqueue()
val existingId: RecipientId = recipientDatabase.getAndPossiblyMergeLegacy(ACI_A, E164_A)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMergeLegacy(ACI_A, E164_B)
assertEquals(existingId, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
assertEquals(E164_B, retrievedRecipient.requireE164())
changeNumberListener.waitForJobManager()
assert(changeNumberListener.numberChangeWasEnqueued)
}
/** High trust lets you merge two different users into one. You should prefer the ACI user. Not shown: merging threads, dropping e164 sessions, etc. */
@Test
fun getAndPossiblyMerge_merge_general() {
// Setup
val recipientIdAci: RecipientId = recipientDatabase.getOrInsertFromServiceId(ACI_A)
val recipientIdE164: RecipientId = recipientDatabase.getOrInsertFromE164(E164_A)
val recipientIdAciB: RecipientId = recipientDatabase.getOrInsertFromServiceId(ACI_B)
val smsId1: Long = smsDatabase.insertMessageInbox(smsMessage(sender = recipientIdAci, time = 0, body = "0")).get().messageId
val smsId2: Long = smsDatabase.insertMessageInbox(smsMessage(sender = recipientIdE164, time = 1, body = "1")).get().messageId
val smsId3: Long = smsDatabase.insertMessageInbox(smsMessage(sender = recipientIdAci, time = 2, body = "2")).get().messageId
val mmsId1: Long = mmsDatabase.insertSecureDecryptedMessageInbox(mmsMessage(sender = recipientIdAci, time = 3, body = "3"), -1).get().messageId
val mmsId2: Long = mmsDatabase.insertSecureDecryptedMessageInbox(mmsMessage(sender = recipientIdE164, time = 4, body = "4"), -1).get().messageId
val mmsId3: Long = mmsDatabase.insertSecureDecryptedMessageInbox(mmsMessage(sender = recipientIdAci, time = 5, body = "5"), -1).get().messageId
val threadIdAci: Long = threadDatabase.getThreadIdFor(recipientIdAci)!!
val threadIdE164: Long = threadDatabase.getThreadIdFor(recipientIdE164)!!
assertNotEquals(threadIdAci, threadIdE164)
mentionDatabase.insert(threadIdAci, mmsId1, listOf(Mention(recipientIdE164, 0, 1)))
mentionDatabase.insert(threadIdE164, mmsId2, listOf(Mention(recipientIdAci, 0, 1)))
groupReceiptDatabase.insert(listOf(recipientIdAci, recipientIdE164), mmsId1, 0, 3)
val identityKeyAci: IdentityKey = identityKey(1)
val identityKeyE164: IdentityKey = identityKey(2)
identityDatabase.saveIdentity(ACI_A.toString(), recipientIdAci, identityKeyAci, IdentityDatabase.VerifiedStatus.VERIFIED, false, 0, false)
identityDatabase.saveIdentity(E164_A, recipientIdE164, identityKeyE164, IdentityDatabase.VerifiedStatus.VERIFIED, false, 0, false)
sessionDatabase.store(localAci, SignalProtocolAddress(ACI_A.toString(), 1), SessionRecord())
reactionDatabase.addReaction(MessageId(smsId1, false), ReactionRecord("a", recipientIdAci, 1, 1))
reactionDatabase.addReaction(MessageId(mmsId1, true), ReactionRecord("b", recipientIdE164, 1, 1))
val profile1: NotificationProfile = notificationProfile(name = "Test")
val profile2: NotificationProfile = notificationProfile(name = "Test2")
notificationProfileDatabase.addAllowedRecipient(profileId = profile1.id, recipientId = recipientIdAci)
notificationProfileDatabase.addAllowedRecipient(profileId = profile1.id, recipientId = recipientIdE164)
notificationProfileDatabase.addAllowedRecipient(profileId = profile2.id, recipientId = recipientIdE164)
notificationProfileDatabase.addAllowedRecipient(profileId = profile2.id, recipientId = recipientIdAciB)
val distributionListId: DistributionListId = distributionListDatabase.createList("testlist", listOf(recipientIdE164, recipientIdAciB))!!
// Merge
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMergeLegacy(ACI_A, E164_A, true)
val retrievedThreadId: Long = threadDatabase.getThreadIdFor(retrievedId)!!
assertEquals(recipientIdAci, retrievedId)
// Recipient validation
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
assertEquals(E164_A, retrievedRecipient.requireE164())
val existingE164Recipient = Recipient.resolved(recipientIdE164)
assertEquals(retrievedId, existingE164Recipient.id)
// Thread validation
assertEquals(threadIdAci, retrievedThreadId)
Assert.assertNull(threadDatabase.getThreadIdFor(recipientIdE164))
Assert.assertNull(threadDatabase.getThreadRecord(threadIdE164))
// SMS validation
val sms1: MessageRecord = smsDatabase.getMessageRecord(smsId1)!!
val sms2: MessageRecord = smsDatabase.getMessageRecord(smsId2)!!
val sms3: MessageRecord = smsDatabase.getMessageRecord(smsId3)!!
assertEquals(retrievedId, sms1.recipient.id)
assertEquals(retrievedId, sms2.recipient.id)
assertEquals(retrievedId, sms3.recipient.id)
assertEquals(retrievedThreadId, sms1.threadId)
assertEquals(retrievedThreadId, sms2.threadId)
assertEquals(retrievedThreadId, sms3.threadId)
// MMS validation
val mms1: MessageRecord = mmsDatabase.getMessageRecord(mmsId1)!!
val mms2: MessageRecord = mmsDatabase.getMessageRecord(mmsId2)!!
val mms3: MessageRecord = mmsDatabase.getMessageRecord(mmsId3)!!
assertEquals(retrievedId, mms1.recipient.id)
assertEquals(retrievedId, mms2.recipient.id)
assertEquals(retrievedId, mms3.recipient.id)
assertEquals(retrievedThreadId, mms1.threadId)
assertEquals(retrievedThreadId, mms2.threadId)
assertEquals(retrievedThreadId, mms3.threadId)
// Mention validation
val mention1: MentionModel = getMention(mmsId1)
assertEquals(retrievedId, mention1.recipientId)
assertEquals(retrievedThreadId, mention1.threadId)
val mention2: MentionModel = getMention(mmsId2)
assertEquals(retrievedId, mention2.recipientId)
assertEquals(retrievedThreadId, mention2.threadId)
// Group receipt validation
val groupReceipts: List<GroupReceiptDatabase.GroupReceiptInfo> = groupReceiptDatabase.getGroupReceiptInfo(mmsId1)
assertEquals(retrievedId, groupReceipts[0].recipientId)
assertEquals(retrievedId, groupReceipts[1].recipientId)
// Identity validation
assertEquals(identityKeyAci, identityDatabase.getIdentityStoreRecord(ACI_A.toString())!!.identityKey)
Assert.assertNull(identityDatabase.getIdentityStoreRecord(E164_A))
// Session validation
Assert.assertNotNull(sessionDatabase.load(localAci, SignalProtocolAddress(ACI_A.toString(), 1)))
// Reaction validation
val reactionsSms: List<ReactionRecord> = reactionDatabase.getReactions(MessageId(smsId1, false))
val reactionsMms: List<ReactionRecord> = reactionDatabase.getReactions(MessageId(mmsId1, true))
assertEquals(1, reactionsSms.size)
assertEquals(ReactionRecord("a", recipientIdAci, 1, 1), reactionsSms[0])
assertEquals(1, reactionsMms.size)
assertEquals(ReactionRecord("b", recipientIdAci, 1, 1), reactionsMms[0])
// Notification Profile validation
val updatedProfile1: NotificationProfile = notificationProfileDatabase.getProfile(profile1.id)!!
val updatedProfile2: NotificationProfile = notificationProfileDatabase.getProfile(profile2.id)!!
MatcherAssert.assertThat("Notification Profile 1 should now only contain ACI $recipientIdAci", updatedProfile1.allowedMembers, Matchers.containsInAnyOrder(recipientIdAci))
MatcherAssert.assertThat("Notification Profile 2 should now contain ACI A ($recipientIdAci) and ACI B ($recipientIdAciB)", updatedProfile2.allowedMembers, Matchers.containsInAnyOrder(recipientIdAci, recipientIdAciB))
// Distribution List validation
val updatedList: DistributionListRecord = distributionListDatabase.getList(distributionListId)!!
MatcherAssert.assertThat("Distribution list should have updated $recipientIdE164 to $recipientIdAci", updatedList.members, Matchers.containsInAnyOrder(recipientIdAci, recipientIdAciB))
}
// ==============================================================
// Misc
// ==============================================================
@Test
fun createByE164SanityCheck() {
// GIVEN one recipient
val recipientId: RecipientId = recipientDatabase.getOrInsertFromE164(E164_A)
// WHEN I retrieve one by E164
val possible: Optional<RecipientId> = recipientDatabase.getByE164(E164_A)
// THEN I get it back, and it has the properties I expect
assertTrue(possible.isPresent)
assertEquals(recipientId, possible.get())
val recipient = Recipient.resolved(recipientId)
assertTrue(recipient.e164.isPresent)
assertEquals(E164_A, recipient.e164.get())
}
@Test
fun createByUuidSanityCheck() {
// GIVEN one recipient
val recipientId: RecipientId = recipientDatabase.getOrInsertFromServiceId(ACI_A)
// WHEN I retrieve one by UUID
val possible: Optional<RecipientId> = recipientDatabase.getByServiceId(ACI_A)
// THEN I get it back, and it has the properties I expect
assertTrue(possible.isPresent)
assertEquals(recipientId, possible.get())
val recipient = Recipient.resolved(recipientId)
assertTrue(recipient.serviceId.isPresent)
assertEquals(ACI_A, recipient.serviceId.get())
}
@Test(expected = IllegalArgumentException::class)
fun getAndPossiblyMerge_noArgs_invalid() {
recipientDatabase.getAndPossiblyMergeLegacy(null, null, true)
}
private fun ensureDbEmpty() {
SignalDatabase.rawDatabase.rawQuery("SELECT COUNT(*) FROM ${RecipientDatabase.TABLE_NAME} WHERE ${RecipientDatabase.DISTRIBUTION_LIST_ID} IS NULL ", null).use { cursor ->
assertTrue(cursor.moveToFirst())
assertEquals(0, cursor.getLong(0))
}
}
private fun smsMessage(sender: RecipientId, time: Long = 0, body: String = "", groupId: Optional<GroupId> = Optional.empty()): IncomingTextMessage {
return IncomingTextMessage(sender, 1, time, time, time, body, groupId, 0, true, null)
}
private fun mmsMessage(sender: RecipientId, time: Long = 0, body: String = "", groupId: Optional<GroupId> = Optional.empty()): IncomingMediaMessage {
return IncomingMediaMessage(sender, groupId, body, time, time, time, emptyList(), 0, 0, false, false, true, Optional.empty())
}
private fun identityKey(value: Byte): IdentityKey {
val bytes = ByteArray(33)
bytes[0] = 0x05
bytes[1] = value
return IdentityKey(bytes)
}
private fun notificationProfile(name: String): NotificationProfile {
return (notificationProfileDatabase.createProfile(name = name, emoji = "", color = AvatarColor.A210, System.currentTimeMillis()) as NotificationProfileDatabase.NotificationProfileChangeResult.Success).notificationProfile
}
private fun groupMasterKey(value: Byte): GroupMasterKey {
val bytes = ByteArray(32)
bytes[0] = value
return GroupMasterKey(bytes)
}
private fun decryptedGroup(members: Collection<UUID>): DecryptedGroup {
return DecryptedGroup.newBuilder()
.addAllMembers(members.map { DecryptedMember.newBuilder().setUuid(UuidUtil.toByteString(it)).build() })
.build()
}
private fun getMention(messageId: Long): MentionModel {
SignalDatabase.rawDatabase.rawQuery("SELECT * FROM ${MentionDatabase.TABLE_NAME} WHERE ${MentionDatabase.MESSAGE_ID} = $messageId").use { cursor ->
cursor.moveToFirst()
return MentionModel(
recipientId = RecipientId.from(CursorUtil.requireLong(cursor, MentionDatabase.RECIPIENT_ID)),
threadId = CursorUtil.requireLong(cursor, MentionDatabase.THREAD_ID)
)
}
}
/** The normal mention model doesn't have a threadId, so we need to do it ourselves for the test */
data class MentionModel(
val recipientId: RecipientId,
val threadId: Long
)
private class ChangeNumberListener {
var numberChangeWasEnqueued = false
private set
fun waitForJobManager() {
ApplicationDependencies.getJobManager().flush()
ThreadUtil.sleep(500)
}
fun enqueue() {
ApplicationDependencies.getJobManager().addListener(
{ job -> job.factoryKey == RecipientChangedNumberJob.KEY },
{ _, _ -> numberChangeWasEnqueued = true }
)
}
}
companion object {
val ACI_A = ACI.from(UUID.fromString("3436efbe-5a76-47fa-a98a-7e72c948a82e"))
val ACI_B = ACI.from(UUID.fromString("8de7f691-0b60-4a68-9cd9-ed2f8453f9ed"))
val PNI_A = PNI.from(UUID.fromString("154b8d92-c960-4f6c-8385-671ad2ffb999"))
val PNI_B = PNI.from(UUID.fromString("ba92b1fb-cd55-40bf-adda-c35a85375533"))
const val E164_A = "+12221234567"
const val E164_B = "+13331234567"
}
}

View File

@@ -1,671 +0,0 @@
package org.thoughtcrime.securesms.database
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.hamcrest.MatcherAssert
import org.hamcrest.Matchers
import org.junit.Assert
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.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.signal.core.util.CursorUtil
import org.signal.core.util.ThreadUtil
import org.signal.libsignal.protocol.IdentityKey
import org.signal.libsignal.protocol.SignalProtocolAddress
import org.signal.libsignal.protocol.state.SessionRecord
import org.signal.libsignal.zkgroup.groups.GroupMasterKey
import org.signal.storageservice.protos.groups.local.DecryptedGroup
import org.signal.storageservice.protos.groups.local.DecryptedMember
import org.thoughtcrime.securesms.conversation.colors.AvatarColor
import org.thoughtcrime.securesms.database.model.DistributionListId
import org.thoughtcrime.securesms.database.model.DistributionListRecord
import org.thoughtcrime.securesms.database.model.Mention
import org.thoughtcrime.securesms.database.model.MessageId
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.database.model.ReactionRecord
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.jobs.RecipientChangedNumberJob
import org.thoughtcrime.securesms.keyvalue.AccountValues
import org.thoughtcrime.securesms.keyvalue.KeyValueDataSet
import org.thoughtcrime.securesms.keyvalue.KeyValueStore
import org.thoughtcrime.securesms.keyvalue.MockKeyValuePersistentStorage
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.mms.IncomingMediaMessage
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.sms.IncomingTextMessage
import org.whispersystems.signalservice.api.push.ACI
import org.whispersystems.signalservice.api.push.PNI
import org.whispersystems.signalservice.api.util.UuidUtil
import java.util.Optional
import java.util.UUID
@RunWith(AndroidJUnit4::class)
class RecipientDatabaseTest_getAndPossiblyMergePnp {
private lateinit var recipientDatabase: RecipientDatabase
private lateinit var identityDatabase: IdentityDatabase
private lateinit var groupReceiptDatabase: GroupReceiptDatabase
private lateinit var groupDatabase: GroupDatabase
private lateinit var threadDatabase: ThreadDatabase
private lateinit var smsDatabase: MessageDatabase
private lateinit var mmsDatabase: MessageDatabase
private lateinit var sessionDatabase: SessionDatabase
private lateinit var mentionDatabase: MentionDatabase
private lateinit var reactionDatabase: ReactionDatabase
private lateinit var notificationProfileDatabase: NotificationProfileDatabase
private lateinit var distributionListDatabase: DistributionListDatabase
private val localAci = ACI.from(UUID.randomUUID())
private val localPni = PNI.from(UUID.randomUUID())
@Before
fun setup() {
recipientDatabase = SignalDatabase.recipients
recipientDatabase = SignalDatabase.recipients
identityDatabase = SignalDatabase.identities
groupReceiptDatabase = SignalDatabase.groupReceipts
groupDatabase = SignalDatabase.groups
threadDatabase = SignalDatabase.threads
smsDatabase = SignalDatabase.sms
mmsDatabase = SignalDatabase.mms
sessionDatabase = SignalDatabase.sessions
mentionDatabase = SignalDatabase.mentions
reactionDatabase = SignalDatabase.reactions
notificationProfileDatabase = SignalDatabase.notificationProfiles
distributionListDatabase = SignalDatabase.distributionLists
ensureDbEmpty()
SignalStore.account().setAci(localAci)
SignalStore.account().setPni(localPni)
}
// ==============================================================
// If both the ACI and E164 map to no one
// ==============================================================
/** If all you have is an ACI, you can just store that, regardless of trust level. */
@Test
fun getAndPossiblyMerge_aciAndE164MapToNoOne_aciOnly() {
val recipientId: RecipientId = recipientDatabase.getAndPossiblyMergePnp(ACI_A, null)
val recipient = Recipient.resolved(recipientId)
assertEquals(ACI_A, recipient.requireServiceId())
assertFalse(recipient.hasE164())
}
/** If all you have is an E164, you can just store that, regardless of trust level. */
@Test
fun getAndPossiblyMerge_aciAndE164MapToNoOne_e164Only() {
val recipientId: RecipientId = recipientDatabase.getAndPossiblyMergePnp(null, E164_A)
val recipient = Recipient.resolved(recipientId)
assertEquals(E164_A, recipient.requireE164())
assertFalse(recipient.hasServiceId())
}
/** With high trust, you can associate an ACI-e164 pair. */
@Test
fun getAndPossiblyMerge_aciAndE164MapToNoOne_aciAndE164() {
val recipientId: RecipientId = recipientDatabase.getAndPossiblyMergePnp(ACI_A, E164_A)
val recipient = Recipient.resolved(recipientId)
assertEquals(ACI_A, recipient.requireServiceId())
assertEquals(E164_A, recipient.requireE164())
}
// ==============================================================
// If the ACI maps to an existing user, but the E164 doesn't
// ==============================================================
/** You can associate an e164 with an existing ACI. */
@Test
fun getAndPossiblyMerge_aciMapsToExistingUserButE164DoesNot_aciAndE164() {
val existingId: RecipientId = recipientDatabase.getOrInsertFromServiceId(ACI_A)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMergePnp(ACI_A, E164_A)
assertEquals(existingId, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
assertEquals(E164_A, retrievedRecipient.requireE164())
}
/** Basically the change number case. Update the existing user. */
@Test
fun getAndPossiblyMerge_aciMapsToExistingUserButE164DoesNot_aciAndE164_2() {
val existingId: RecipientId = recipientDatabase.getAndPossiblyMergePnp(ACI_A, E164_A)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMergePnp(ACI_A, E164_B)
assertEquals(existingId, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
assertEquals(E164_B, retrievedRecipient.requireE164())
}
// ==============================================================
// If the E164 maps to an existing user, but the ACI doesn't
// ==============================================================
/** You can associate an e164 with an existing ACI. */
@Test
fun getAndPossiblyMerge_e164MapsToExistingUserButAciDoesNot_aciAndE164() {
val existingId: RecipientId = recipientDatabase.getOrInsertFromE164(E164_A)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMergePnp(ACI_A, E164_A)
assertEquals(existingId, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
assertEquals(E164_A, retrievedRecipient.requireE164())
}
/** We never change the ACI of an existing row. New ACI = new person. Take the e164 from the current holder. */
@Test
fun getAndPossiblyMerge_e164MapsToExistingUserButAciDoesNot_aciAndE164_2() {
val existingId: RecipientId = recipientDatabase.getAndPossiblyMergePnp(ACI_A, E164_A)
recipientDatabase.setPni(existingId, PNI_A)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMergePnp(ACI_B, E164_A)
recipientDatabase.setPni(retrievedId, PNI_A)
assertNotEquals(existingId, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_B, retrievedRecipient.requireServiceId())
assertEquals(E164_A, retrievedRecipient.requireE164())
val existingRecipient = Recipient.resolved(existingId)
assertEquals(ACI_A, existingRecipient.requireServiceId())
assertFalse(existingRecipient.hasE164())
}
/** We never want to remove the e164 of our own contact entry. Leave the e164 alone. */
@Ignore("Change self isn't implemented yet!")
@Test
fun getAndPossiblyMerge_e164MapsToExistingUserButAciDoesNot_e164BelongsToLocalUser() {
val dataSet = KeyValueDataSet().apply {
putString(AccountValues.KEY_E164, E164_A)
putString(AccountValues.KEY_ACI, ACI_A.toString())
}
SignalStore.inject(KeyValueStore(MockKeyValuePersistentStorage.withDataSet(dataSet)))
val existingId: RecipientId = recipientDatabase.getAndPossiblyMergePnp(ACI_A, E164_A)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMergePnp(ACI_B, E164_A)
assertNotEquals(existingId, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_B, retrievedRecipient.requireServiceId())
assertFalse(retrievedRecipient.hasE164())
val existingRecipient = Recipient.resolved(existingId)
assertEquals(ACI_A, existingRecipient.requireServiceId())
assertEquals(E164_A, existingRecipient.requireE164())
}
// ==============================================================
// If both the ACI and E164 map to an existing user
// ==============================================================
/** If your ACI and e164 match, youre good. */
@Test
fun getAndPossiblyMerge_bothAciAndE164MapToExistingUser_aciAndE164() {
val existingId: RecipientId = recipientDatabase.getAndPossiblyMergePnp(ACI_A, E164_A)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMergePnp(ACI_A, E164_A)
assertEquals(existingId, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
assertEquals(E164_A, retrievedRecipient.requireE164())
}
/** Merge two different users into one. You should prefer the ACI user. Not shown: merging threads, dropping e164 sessions, etc. */
@Test
fun getAndPossiblyMerge_bothAciAndE164MapToExistingUser_aciAndE164_merge() {
val changeNumberListener = ChangeNumberListener()
changeNumberListener.enqueue()
val existingAciId: RecipientId = recipientDatabase.getAndPossiblyMergePnp(ACI_A, null)
val existingE164Id: RecipientId = recipientDatabase.getAndPossiblyMergePnp(null, E164_A)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMergePnp(ACI_A, E164_A)
assertEquals(existingAciId, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
assertEquals(E164_A, retrievedRecipient.requireE164())
// TODO: Recipient remapping!
// val existingE164Recipient = Recipient.resolved(existingE164Id)
// assertEquals(retrievedId, existingE164Recipient.id)
changeNumberListener.waitForJobManager()
assertFalse(changeNumberListener.numberChangeWasEnqueued)
}
/** Same as [getAndPossiblyMerge_bothAciAndE164MapToExistingUser_aciAndE164_merge], but with a number change. */
@Test
fun getAndPossiblyMerge_bothAciAndE164MapToExistingUser_aciAndE164_merge_changedNumber() {
val changeNumberListener = ChangeNumberListener()
changeNumberListener.enqueue()
val existingAciId: RecipientId = recipientDatabase.getAndPossiblyMergePnp(ACI_A, E164_B)
val existingE164Id: RecipientId = recipientDatabase.getAndPossiblyMergePnp(null, E164_A)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMergePnp(ACI_A, E164_A)
assertEquals(existingAciId, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
assertEquals(E164_A, retrievedRecipient.requireE164())
// TODO: Recipient remapping!
// val existingE164Recipient = Recipient.resolved(existingE164Id)
// assertEquals(retrievedId, existingE164Recipient.id)
// TODO: Change number!
// changeNumberListener.waitForJobManager()
// assert(changeNumberListener.numberChangeWasEnqueued)
}
/** No new rules here, just a more complex scenario to show how different rules interact. */
@Test
fun getAndPossiblyMerge_bothAciAndE164MapToExistingUser_aciAndE164_complex() {
val changeNumberListener = ChangeNumberListener()
changeNumberListener.enqueue()
val existingId1: RecipientId = recipientDatabase.getAndPossiblyMergePnp(ACI_A, E164_B)
val existingId2: RecipientId = recipientDatabase.getAndPossiblyMergePnp(ACI_B, E164_A)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMergePnp(ACI_A, E164_A)
assertEquals(existingId1, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
assertEquals(E164_A, retrievedRecipient.requireE164())
val existingRecipient2 = Recipient.resolved(existingId2)
assertEquals(ACI_B, existingRecipient2.requireServiceId())
assertFalse(existingRecipient2.hasE164())
// TODO: Change number!
// assert(changeNumberListener.numberChangeWasEnqueued)
}
/**
* Another case that results in a merge. Nothing strictly new here, but this case is called out because its a merge but *also* an E164 change,
* which clients may need to know for UX purposes.
*/
@Test
fun getAndPossiblyMerge_bothAciAndE164MapToExistingUser_aciAndE164_mergeAndPhoneNumberChange() {
val existingId1: RecipientId = recipientDatabase.getAndPossiblyMergePnp(ACI_A, E164_B)
val existingId2: RecipientId = recipientDatabase.getAndPossiblyMergePnp(null, E164_A)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMergePnp(ACI_A, E164_A)
assertEquals(existingId1, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
assertEquals(E164_A, retrievedRecipient.requireE164())
assertFalse(recipientDatabase.getByE164(E164_B).isPresent)
// TODO: Recipient remapping!
// val recipientWithId2 = Recipient.resolved(existingId2)
// assertEquals(retrievedId, recipientWithId2.id)
}
/** We never want to remove the e164 of our own contact entry. Leave the e164 alone. */
@Ignore("Change self isn't implemented yet!")
@Test
fun getAndPossiblyMerge_bothAciAndE164MapToExistingUser_e164BelongsToLocalUser() {
val dataSet = KeyValueDataSet().apply {
putString(AccountValues.KEY_E164, E164_A)
putString(AccountValues.KEY_ACI, ACI_B.toString())
}
SignalStore.inject(KeyValueStore(MockKeyValuePersistentStorage.withDataSet(dataSet)))
val existingId1: RecipientId = recipientDatabase.getAndPossiblyMergePnp(ACI_B, E164_A)
val existingId2: RecipientId = recipientDatabase.getAndPossiblyMergePnp(ACI_A, null)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMergePnp(ACI_A, E164_A)
assertEquals(existingId2, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
assertFalse(retrievedRecipient.hasE164())
val recipientWithId1 = Recipient.resolved(existingId1)
assertEquals(ACI_B, recipientWithId1.requireServiceId())
assertEquals(E164_A, recipientWithId1.requireE164())
}
/** This is a case where normally we'd update the E164 of a user, but here the changeSelf flag is disabled, so we shouldn't. */
@Ignore("Change self isn't implemented yet!")
@Test
fun getAndPossiblyMerge_aciMapsToExistingUserButE164DoesNot_aciBelongsToLocalUser_changeSelfFalse() {
val dataSet = KeyValueDataSet().apply {
putString(AccountValues.KEY_E164, E164_A)
putString(AccountValues.KEY_ACI, ACI_A.toString())
}
SignalStore.inject(KeyValueStore(MockKeyValuePersistentStorage.withDataSet(dataSet)))
val existingId: RecipientId = recipientDatabase.getAndPossiblyMergePnp(ACI_A, E164_A)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMergePnp(ACI_A, E164_B, changeSelf = false)
assertEquals(existingId, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
assertEquals(E164_A, retrievedRecipient.requireE164())
}
/** This is a case where we're changing our own number, and it's allowed because changeSelf = true. */
@Test
fun getAndPossiblyMerge_aciMapsToExistingUserButE164DoesNot_aciBelongsToLocalUser_changeSelfTrue() {
val dataSet = KeyValueDataSet().apply {
putString(AccountValues.KEY_E164, E164_A)
putString(AccountValues.KEY_ACI, ACI_A.toString())
}
SignalStore.inject(KeyValueStore(MockKeyValuePersistentStorage.withDataSet(dataSet)))
val existingId: RecipientId = recipientDatabase.getAndPossiblyMergePnp(ACI_A, E164_A)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMergePnp(ACI_A, E164_B, changeSelf = true)
assertEquals(existingId, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
assertEquals(E164_B, retrievedRecipient.requireE164())
}
/** Verifying a case where a change number job is expected to be enqueued. */
@Test
fun getAndPossiblyMerge_aciMapsToExistingUserButE164DoesNot_changedNumber() {
val changeNumberListener = ChangeNumberListener()
changeNumberListener.enqueue()
val existingId: RecipientId = recipientDatabase.getAndPossiblyMergePnp(ACI_A, E164_A)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMergePnp(ACI_A, E164_B)
assertEquals(existingId, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
assertEquals(E164_B, retrievedRecipient.requireE164())
// TODO: Change number!
// changeNumberListener.waitForJobManager()
// assert(changeNumberListener.numberChangeWasEnqueued)
}
/** High trust lets you merge two different users into one. You should prefer the ACI user. Not shown: merging threads, dropping e164 sessions, etc. */
@Ignore("This level of merging isn't implemented yet!")
@Test
fun getAndPossiblyMerge_merge_general() {
// Setup
val recipientIdAci: RecipientId = recipientDatabase.getOrInsertFromServiceId(ACI_A)
val recipientIdE164: RecipientId = recipientDatabase.getOrInsertFromE164(E164_A)
val recipientIdAciB: RecipientId = recipientDatabase.getOrInsertFromServiceId(ACI_B)
val smsId1: Long = smsDatabase.insertMessageInbox(smsMessage(sender = recipientIdAci, time = 0, body = "0")).get().messageId
val smsId2: Long = smsDatabase.insertMessageInbox(smsMessage(sender = recipientIdE164, time = 1, body = "1")).get().messageId
val smsId3: Long = smsDatabase.insertMessageInbox(smsMessage(sender = recipientIdAci, time = 2, body = "2")).get().messageId
val mmsId1: Long = mmsDatabase.insertSecureDecryptedMessageInbox(mmsMessage(sender = recipientIdAci, time = 3, body = "3"), -1).get().messageId
val mmsId2: Long = mmsDatabase.insertSecureDecryptedMessageInbox(mmsMessage(sender = recipientIdE164, time = 4, body = "4"), -1).get().messageId
val mmsId3: Long = mmsDatabase.insertSecureDecryptedMessageInbox(mmsMessage(sender = recipientIdAci, time = 5, body = "5"), -1).get().messageId
val threadIdAci: Long = threadDatabase.getThreadIdFor(recipientIdAci)!!
val threadIdE164: Long = threadDatabase.getThreadIdFor(recipientIdE164)!!
assertNotEquals(threadIdAci, threadIdE164)
mentionDatabase.insert(threadIdAci, mmsId1, listOf(Mention(recipientIdE164, 0, 1)))
mentionDatabase.insert(threadIdE164, mmsId2, listOf(Mention(recipientIdAci, 0, 1)))
groupReceiptDatabase.insert(listOf(recipientIdAci, recipientIdE164), mmsId1, 0, 3)
val identityKeyAci: IdentityKey = identityKey(1)
val identityKeyE164: IdentityKey = identityKey(2)
identityDatabase.saveIdentity(ACI_A.toString(), recipientIdAci, identityKeyAci, IdentityDatabase.VerifiedStatus.VERIFIED, false, 0, false)
identityDatabase.saveIdentity(E164_A, recipientIdE164, identityKeyE164, IdentityDatabase.VerifiedStatus.VERIFIED, false, 0, false)
sessionDatabase.store(localAci, SignalProtocolAddress(ACI_A.toString(), 1), SessionRecord())
reactionDatabase.addReaction(MessageId(smsId1, false), ReactionRecord("a", recipientIdAci, 1, 1))
reactionDatabase.addReaction(MessageId(mmsId1, true), ReactionRecord("b", recipientIdE164, 1, 1))
val profile1: NotificationProfile = notificationProfile(name = "Test")
val profile2: NotificationProfile = notificationProfile(name = "Test2")
notificationProfileDatabase.addAllowedRecipient(profileId = profile1.id, recipientId = recipientIdAci)
notificationProfileDatabase.addAllowedRecipient(profileId = profile1.id, recipientId = recipientIdE164)
notificationProfileDatabase.addAllowedRecipient(profileId = profile2.id, recipientId = recipientIdE164)
notificationProfileDatabase.addAllowedRecipient(profileId = profile2.id, recipientId = recipientIdAciB)
val distributionListId: DistributionListId = distributionListDatabase.createList("testlist", listOf(recipientIdE164, recipientIdAciB))!!
// Merge
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMergePnp(ACI_A, E164_A, true)
val retrievedThreadId: Long = threadDatabase.getThreadIdFor(retrievedId)!!
assertEquals(recipientIdAci, retrievedId)
// Recipient validation
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
assertEquals(E164_A, retrievedRecipient.requireE164())
val existingE164Recipient = Recipient.resolved(recipientIdE164)
assertEquals(retrievedId, existingE164Recipient.id)
// Thread validation
assertEquals(threadIdAci, retrievedThreadId)
Assert.assertNull(threadDatabase.getThreadIdFor(recipientIdE164))
Assert.assertNull(threadDatabase.getThreadRecord(threadIdE164))
// SMS validation
val sms1: MessageRecord = smsDatabase.getMessageRecord(smsId1)!!
val sms2: MessageRecord = smsDatabase.getMessageRecord(smsId2)!!
val sms3: MessageRecord = smsDatabase.getMessageRecord(smsId3)!!
assertEquals(retrievedId, sms1.recipient.id)
assertEquals(retrievedId, sms2.recipient.id)
assertEquals(retrievedId, sms3.recipient.id)
assertEquals(retrievedThreadId, sms1.threadId)
assertEquals(retrievedThreadId, sms2.threadId)
assertEquals(retrievedThreadId, sms3.threadId)
// MMS validation
val mms1: MessageRecord = mmsDatabase.getMessageRecord(mmsId1)!!
val mms2: MessageRecord = mmsDatabase.getMessageRecord(mmsId2)!!
val mms3: MessageRecord = mmsDatabase.getMessageRecord(mmsId3)!!
assertEquals(retrievedId, mms1.recipient.id)
assertEquals(retrievedId, mms2.recipient.id)
assertEquals(retrievedId, mms3.recipient.id)
assertEquals(retrievedThreadId, mms1.threadId)
assertEquals(retrievedThreadId, mms2.threadId)
assertEquals(retrievedThreadId, mms3.threadId)
// Mention validation
val mention1: MentionModel = getMention(mmsId1)
assertEquals(retrievedId, mention1.recipientId)
assertEquals(retrievedThreadId, mention1.threadId)
val mention2: MentionModel = getMention(mmsId2)
assertEquals(retrievedId, mention2.recipientId)
assertEquals(retrievedThreadId, mention2.threadId)
// Group receipt validation
val groupReceipts: List<GroupReceiptDatabase.GroupReceiptInfo> = groupReceiptDatabase.getGroupReceiptInfo(mmsId1)
assertEquals(retrievedId, groupReceipts[0].recipientId)
assertEquals(retrievedId, groupReceipts[1].recipientId)
// Identity validation
assertEquals(identityKeyAci, identityDatabase.getIdentityStoreRecord(ACI_A.toString())!!.identityKey)
Assert.assertNull(identityDatabase.getIdentityStoreRecord(E164_A))
// Session validation
Assert.assertNotNull(sessionDatabase.load(localAci, SignalProtocolAddress(ACI_A.toString(), 1)))
// Reaction validation
val reactionsSms: List<ReactionRecord> = reactionDatabase.getReactions(MessageId(smsId1, false))
val reactionsMms: List<ReactionRecord> = reactionDatabase.getReactions(MessageId(mmsId1, true))
assertEquals(1, reactionsSms.size)
assertEquals(ReactionRecord("a", recipientIdAci, 1, 1), reactionsSms[0])
assertEquals(1, reactionsMms.size)
assertEquals(ReactionRecord("b", recipientIdAci, 1, 1), reactionsMms[0])
// Notification Profile validation
val updatedProfile1: NotificationProfile = notificationProfileDatabase.getProfile(profile1.id)!!
val updatedProfile2: NotificationProfile = notificationProfileDatabase.getProfile(profile2.id)!!
MatcherAssert.assertThat("Notification Profile 1 should now only contain ACI $recipientIdAci", updatedProfile1.allowedMembers, Matchers.containsInAnyOrder(recipientIdAci))
MatcherAssert.assertThat("Notification Profile 2 should now contain ACI A ($recipientIdAci) and ACI B ($recipientIdAciB)", updatedProfile2.allowedMembers, Matchers.containsInAnyOrder(recipientIdAci, recipientIdAciB))
// Distribution List validation
val updatedList: DistributionListRecord = distributionListDatabase.getList(distributionListId)!!
MatcherAssert.assertThat("Distribution list should have updated $recipientIdE164 to $recipientIdAci", updatedList.members, Matchers.containsInAnyOrder(recipientIdAci, recipientIdAciB))
}
// ==============================================================
// Misc
// ==============================================================
@Test
fun createByE164SanityCheck() {
// GIVEN one recipient
val recipientId: RecipientId = recipientDatabase.getOrInsertFromE164(E164_A)
// WHEN I retrieve one by E164
val possible: Optional<RecipientId> = recipientDatabase.getByE164(E164_A)
// THEN I get it back, and it has the properties I expect
assertTrue(possible.isPresent)
assertEquals(recipientId, possible.get())
val recipient = Recipient.resolved(recipientId)
assertTrue(recipient.e164.isPresent)
assertEquals(E164_A, recipient.e164.get())
}
@Test
fun createByUuidSanityCheck() {
// GIVEN one recipient
val recipientId: RecipientId = recipientDatabase.getOrInsertFromServiceId(ACI_A)
// WHEN I retrieve one by UUID
val possible: Optional<RecipientId> = recipientDatabase.getByServiceId(ACI_A)
// THEN I get it back, and it has the properties I expect
assertTrue(possible.isPresent)
assertEquals(recipientId, possible.get())
val recipient = Recipient.resolved(recipientId)
assertTrue(recipient.serviceId.isPresent)
assertEquals(ACI_A, recipient.serviceId.get())
}
@Test(expected = IllegalArgumentException::class)
fun getAndPossiblyMerge_noArgs_invalid() {
recipientDatabase.getAndPossiblyMergePnp(null, null, true)
}
private fun ensureDbEmpty() {
SignalDatabase.rawDatabase.rawQuery("SELECT COUNT(*) FROM ${RecipientDatabase.TABLE_NAME} WHERE ${RecipientDatabase.DISTRIBUTION_LIST_ID} IS NULL ", null).use { cursor ->
assertTrue(cursor.moveToFirst())
assertEquals(0, cursor.getLong(0))
}
}
private fun smsMessage(sender: RecipientId, time: Long = 0, body: String = "", groupId: Optional<GroupId> = Optional.empty()): IncomingTextMessage {
return IncomingTextMessage(sender, 1, time, time, time, body, groupId, 0, true, null)
}
private fun mmsMessage(sender: RecipientId, time: Long = 0, body: String = "", groupId: Optional<GroupId> = Optional.empty()): IncomingMediaMessage {
return IncomingMediaMessage(sender, groupId, body, time, time, time, emptyList(), 0, 0, false, false, true, Optional.empty())
}
private fun identityKey(value: Byte): IdentityKey {
val bytes = ByteArray(33)
bytes[0] = 0x05
bytes[1] = value
return IdentityKey(bytes)
}
private fun notificationProfile(name: String): NotificationProfile {
return (notificationProfileDatabase.createProfile(name = name, emoji = "", color = AvatarColor.A210, System.currentTimeMillis()) as NotificationProfileDatabase.NotificationProfileChangeResult.Success).notificationProfile
}
private fun groupMasterKey(value: Byte): GroupMasterKey {
val bytes = ByteArray(32)
bytes[0] = value
return GroupMasterKey(bytes)
}
private fun decryptedGroup(members: Collection<UUID>): DecryptedGroup {
return DecryptedGroup.newBuilder()
.addAllMembers(members.map { DecryptedMember.newBuilder().setUuid(UuidUtil.toByteString(it)).build() })
.build()
}
private fun getMention(messageId: Long): MentionModel {
SignalDatabase.rawDatabase.rawQuery("SELECT * FROM ${MentionDatabase.TABLE_NAME} WHERE ${MentionDatabase.MESSAGE_ID} = $messageId").use { cursor ->
cursor.moveToFirst()
return MentionModel(
recipientId = RecipientId.from(CursorUtil.requireLong(cursor, MentionDatabase.RECIPIENT_ID)),
threadId = CursorUtil.requireLong(cursor, MentionDatabase.THREAD_ID)
)
}
}
/** The normal mention model doesn't have a threadId, so we need to do it ourselves for the test */
data class MentionModel(
val recipientId: RecipientId,
val threadId: Long
)
private class ChangeNumberListener {
var numberChangeWasEnqueued = false
private set
fun waitForJobManager() {
ApplicationDependencies.getJobManager().flush()
ThreadUtil.sleep(500)
}
fun enqueue() {
ApplicationDependencies.getJobManager().addListener(
{ job -> job.factoryKey == RecipientChangedNumberJob.KEY },
{ _, _ -> numberChangeWasEnqueued = true }
)
}
}
companion object {
val ACI_A = ACI.from(UUID.fromString("3436efbe-5a76-47fa-a98a-7e72c948a82e"))
val ACI_B = ACI.from(UUID.fromString("8de7f691-0b60-4a68-9cd9-ed2f8453f9ed"))
val PNI_A = PNI.from(UUID.fromString("154b8d92-c960-4f6c-8385-671ad2ffb999"))
val PNI_B = PNI.from(UUID.fromString("ba92b1fb-cd55-40bf-adda-c35a85375533"))
const val E164_A = "+12221234567"
const val E164_B = "+13331234567"
}
}

View File

@@ -1,459 +0,0 @@
package org.thoughtcrime.securesms.database
import androidx.core.content.contentValuesOf
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Assert.assertEquals
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.requireLong
import org.signal.core.util.requireString
import org.signal.core.util.select
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.recipients.RecipientId
import org.whispersystems.signalservice.api.push.ACI
import org.whispersystems.signalservice.api.push.PNI
import org.whispersystems.signalservice.api.push.ServiceId
import java.lang.IllegalArgumentException
import java.util.UUID
@RunWith(AndroidJUnit4::class)
class RecipientDatabaseTest_processPnpTuple {
private lateinit var recipientDatabase: RecipientDatabase
private val localAci = ACI.from(UUID.randomUUID())
private val localPni = PNI.from(UUID.randomUUID())
@Before
fun setup() {
recipientDatabase = SignalDatabase.recipients
ensureDbEmpty()
SignalStore.account().setAci(localAci)
SignalStore.account().setPni(localPni)
}
@Test
fun noMatch_e164Only() {
test {
process(E164_A, null, null)
expect(E164_A, null, null)
}
}
@Test
fun noMatch_e164AndPni() {
test {
process(E164_A, PNI_A, null)
expect(E164_A, PNI_A, null)
}
}
@Test
fun noMatch_aciOnly() {
test {
process(null, null, ACI_A)
expect(null, null, ACI_A)
}
}
@Test(expected = IllegalArgumentException::class)
fun noMatch_pniOnly() {
test {
process(null, PNI_A, null)
}
}
@Test(expected = IllegalArgumentException::class)
fun noMatch_noData() {
test {
process(null, null, null)
}
}
@Test
fun noMatch_allFields() {
test {
process(E164_A, PNI_A, ACI_A)
expect(E164_A, PNI_A, ACI_A)
}
}
@Test
fun fullMatch() {
test {
given(E164_A, PNI_A, ACI_A)
process(E164_A, PNI_A, ACI_A)
expect(E164_A, PNI_A, ACI_A)
}
}
@Test
fun onlyE164Matches() {
test {
given(E164_A, null, null)
process(E164_A, PNI_A, ACI_A)
expect(E164_A, PNI_A, ACI_A)
}
}
@Test
fun onlyE164Matches_differentAci() {
test {
given(E164_A, null, ACI_B)
process(E164_A, PNI_A, ACI_A)
expect(null, null, ACI_B)
expect(E164_A, PNI_A, ACI_A)
}
}
@Test
fun e164AndPniMatches() {
test {
given(E164_A, PNI_A, null)
process(E164_A, PNI_A, ACI_A)
expect(E164_A, PNI_A, ACI_A)
}
}
@Test
fun e164AndAciMatches() {
test {
given(E164_A, null, ACI_A)
process(E164_A, PNI_A, ACI_A)
expect(E164_A, PNI_A, ACI_A)
}
}
@Test
fun onlyPniMatches() {
test {
given(null, PNI_A, null)
process(E164_A, PNI_A, ACI_A)
expect(E164_A, PNI_A, ACI_A)
}
}
@Test
fun pniAndAciMatches() {
test {
given(null, PNI_A, ACI_A)
process(E164_A, PNI_A, ACI_A)
expect(E164_A, PNI_A, ACI_A)
}
}
@Test
fun onlyAciMatches() {
test {
given(null, null, ACI_A)
process(E164_A, PNI_A, ACI_A)
expect(E164_A, PNI_A, ACI_A)
}
}
@Test
fun onlyE164Matches_pniChanges_noAciProvided_noPniSession() {
test {
given(E164_A, PNI_B, null)
process(E164_A, PNI_A, null)
expect(E164_A, PNI_A, null)
}
}
@Test
fun e164AndPniMatches_noExistingSession() {
test {
given(E164_A, PNI_A, null)
process(E164_A, PNI_A, ACI_A)
expect(E164_A, PNI_A, ACI_A)
}
}
@Test
fun onlyPniMatches_noExistingSession() {
test {
given(null, PNI_A, null)
process(E164_A, PNI_A, ACI_A)
expect(E164_A, PNI_A, ACI_A)
}
}
@Test
fun onlyPniMatches_noExistingPniSession_changeNumber() {
// This test, I could go either way. We decide to change the E164 on the existing row rather than create a new one.
// But it's an "unstable E164->PNI mapping" case, which we don't expect, so as long as there's a user-visible impact that should be fine.
// TODO Verify change number
test {
given(E164_B, PNI_A, null)
process(E164_A, PNI_A, ACI_A)
expect(E164_A, PNI_A, ACI_A)
}
}
@Test
fun pniAndAciMatches_changeNumber() {
// This test, I could go either way. We decide to change the E164 on the existing row rather than create a new one.
// But it's an "unstable E164->PNI mapping" case, which we don't expect, so as long as there's a user-visible impact that should be fine.
// TODO Verify change number
test {
given(E164_B, PNI_A, ACI_A)
process(E164_A, PNI_A, ACI_A)
expect(E164_A, PNI_A, ACI_A)
}
}
@Test
fun onlyAciMatches_changeNumber() {
// TODO Verify change number
test {
given(E164_B, null, ACI_A)
process(E164_A, PNI_A, ACI_A)
expect(E164_A, PNI_A, ACI_A)
}
}
@Test
fun merge_e164Only_pniOnly_aciOnly() {
test {
given(E164_A, null, null)
given(null, PNI_A, null)
given(null, null, ACI_A)
process(E164_A, PNI_A, ACI_A)
expectDeleted()
expectDeleted()
expect(E164_A, PNI_A, ACI_A)
}
}
@Test
fun merge_e164Only_pniOnly_noAciProvided() {
test {
given(E164_A, null, null)
given(null, PNI_A, null)
process(E164_A, PNI_A, null)
expect(E164_A, PNI_A, null)
expectDeleted()
}
}
@Test
fun merge_e164Only_pniOnly_aciProvidedButNoAciRecord() {
test {
given(E164_A, null, null)
given(null, PNI_A, null)
process(E164_A, PNI_A, ACI_A)
expect(E164_A, PNI_A, ACI_A)
expectDeleted()
}
}
@Test
fun merge_e164Only_pniAndE164_noAciProvided() {
test {
given(E164_A, null, null)
given(E164_B, PNI_A, null)
process(E164_A, PNI_A, null)
expect(E164_A, PNI_A, null)
expect(E164_B, null, null)
}
}
@Test
fun merge_e164AndPni_pniOnly_noAciProvided() {
test {
given(E164_A, PNI_B, null)
given(null, PNI_A, null)
process(E164_A, PNI_A, null)
expect(E164_A, PNI_A, null)
expectDeleted()
}
}
@Test
fun merge_e164AndPni_e164AndPni_noAciProvided_noSessions() {
test {
given(E164_A, PNI_B, null)
given(E164_B, PNI_A, null)
process(E164_A, PNI_A, null)
expect(E164_A, PNI_A, null)
expect(E164_B, null, null)
}
}
@Test
fun merge_e164AndPni_aciOnly() {
test {
given(E164_A, PNI_A, null)
given(null, null, ACI_A)
process(E164_A, PNI_A, ACI_A)
expectDeleted()
expect(E164_A, PNI_A, ACI_A)
}
}
@Test
fun merge_e164AndPni_aciOnly_e164RecordHasSeparateE164() {
test {
given(E164_B, PNI_A, null)
given(null, null, ACI_A)
process(E164_A, PNI_A, ACI_A)
expect(E164_B, null, null)
expect(E164_A, PNI_A, ACI_A)
}
}
@Test
fun merge_e164AndPni_e164AndPniAndAci_changeNumber() {
// TODO Verify change number
test {
given(E164_A, PNI_A, null)
given(E164_B, PNI_B, ACI_A)
process(E164_A, PNI_A, ACI_A)
expectDeleted()
expect(E164_A, PNI_A, ACI_A)
}
}
@Test
fun merge_e164AndPni_e164Aci_changeNumber() {
// TODO Verify change number
test {
given(E164_A, PNI_A, null)
given(E164_B, null, ACI_A)
process(E164_A, PNI_A, ACI_A)
expectDeleted()
expect(E164_A, PNI_A, ACI_A)
}
}
private fun insert(e164: String?, pni: PNI?, aci: ACI?): RecipientId {
val id: Long = SignalDatabase.rawDatabase.insert(
RecipientDatabase.TABLE_NAME,
null,
contentValuesOf(
RecipientDatabase.PHONE to e164,
RecipientDatabase.SERVICE_ID to (aci ?: pni)?.toString(),
RecipientDatabase.PNI_COLUMN to pni?.toString(),
RecipientDatabase.REGISTERED to RecipientDatabase.RegisteredState.REGISTERED.id
)
)
return RecipientId.from(id)
}
private fun require(id: RecipientId): IdRecord {
return get(id)!!
}
private fun get(id: RecipientId): IdRecord? {
SignalDatabase.rawDatabase
.select(RecipientDatabase.ID, RecipientDatabase.PHONE, RecipientDatabase.SERVICE_ID, RecipientDatabase.PNI_COLUMN)
.from(RecipientDatabase.TABLE_NAME)
.where("${RecipientDatabase.ID} = ?", id)
.run()
.use { cursor ->
return if (cursor.moveToFirst()) {
IdRecord(
id = RecipientId.from(cursor.requireLong(RecipientDatabase.ID)),
e164 = cursor.requireString(RecipientDatabase.PHONE),
sid = ServiceId.parseOrNull(cursor.requireString(RecipientDatabase.SERVICE_ID)),
pni = PNI.parseOrNull(cursor.requireString(RecipientDatabase.PNI_COLUMN))
)
} else {
null
}
}
}
private fun ensureDbEmpty() {
SignalDatabase.rawDatabase.rawQuery("SELECT COUNT(*) FROM ${RecipientDatabase.TABLE_NAME} WHERE ${RecipientDatabase.DISTRIBUTION_LIST_ID} IS NULL ", null).use { cursor ->
assertTrue(cursor.moveToFirst())
assertEquals(0, cursor.getLong(0))
}
}
/**
* Baby DSL for making tests readable.
*/
private fun test(init: TestCase.() -> Unit): TestCase {
val test = TestCase()
test.init()
return test
}
private inner class TestCase {
private val generatedIds: LinkedHashSet<RecipientId> = LinkedHashSet()
private var expectCount = 0
fun given(e164: String?, pni: PNI?, aci: ACI?) {
generatedIds += insert(e164, pni, aci)
}
fun process(e164: String?, pni: PNI?, aci: ACI?) {
generatedIds += recipientDatabase.processPnpTuple(e164, pni, aci, false)
}
fun expect(e164: String?, pni: PNI?, aci: ACI?) {
expect(generatedIds.elementAt(expectCount++), e164, pni, aci)
}
fun expect(id: RecipientId, e164: String?, pni: PNI?, aci: ACI?) {
val record: IdRecord = require(id)
assertEquals(e164, record.e164)
assertEquals(pni, record.pni)
assertEquals(aci ?: pni, record.sid)
}
fun expectDeleted() {
expectDeleted(generatedIds.elementAt(expectCount++))
}
fun expectDeleted(id: RecipientId) {
assertNull(get(id))
}
}
private data class IdRecord(
val id: RecipientId,
val e164: String?,
val sid: ServiceId?,
val pni: PNI?,
)
companion object {
val ACI_A = ACI.from(UUID.fromString("3436efbe-5a76-47fa-a98a-7e72c948a82e"))
val ACI_B = ACI.from(UUID.fromString("8de7f691-0b60-4a68-9cd9-ed2f8453f9ed"))
val PNI_A = PNI.from(UUID.fromString("154b8d92-c960-4f6c-8385-671ad2ffb999"))
val PNI_B = PNI.from(UUID.fromString("ba92b1fb-cd55-40bf-adda-c35a85375533"))
const val E164_A = "+12221234567"
const val E164_B = "+13331234567"
}
}

View File

@@ -1,810 +0,0 @@
package org.thoughtcrime.securesms.database
import androidx.core.content.contentValuesOf
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.testing.SignalDatabaseRule
import org.thoughtcrime.securesms.util.Util
import org.whispersystems.signalservice.api.push.ACI
import org.whispersystems.signalservice.api.push.PNI
import org.whispersystems.signalservice.api.push.ServiceId
import java.lang.AssertionError
import java.lang.IllegalArgumentException
import java.lang.IllegalStateException
import java.util.UUID
@RunWith(AndroidJUnit4::class)
class RecipientDatabaseTest_processPnpTupleToChangeSet {
@Rule
@JvmField
val databaseRule = SignalDatabaseRule(deleteAllThreadsOnEachRun = false)
private lateinit var db: RecipientDatabase
@Before
fun setup() {
db = SignalDatabase.recipients
}
@Test
fun noMatch_e164Only() {
val changeSet = db.processPnpTupleToChangeSet(E164_A, null, null, pniVerified = false)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpInsert(E164_A, null, null)
),
changeSet
)
}
@Test
fun noMatch_e164AndPni() {
val changeSet = db.processPnpTupleToChangeSet(E164_A, PNI_A, null, pniVerified = false)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpInsert(E164_A, PNI_A, null)
),
changeSet
)
}
@Test
fun noMatch_aciOnly() {
val changeSet = db.processPnpTupleToChangeSet(null, null, ACI_A, pniVerified = false)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpInsert(null, null, ACI_A)
),
changeSet
)
}
@Test(expected = IllegalArgumentException::class)
fun noMatch_pniOnly() {
db.processPnpTupleToChangeSet(null, PNI_A, null, pniVerified = false)
}
@Test(expected = IllegalArgumentException::class)
fun noMatch_noData() {
db.processPnpTupleToChangeSet(null, null, null, pniVerified = false)
}
@Test
fun noMatch_allFields() {
val changeSet = db.processPnpTupleToChangeSet(E164_A, PNI_A, ACI_A, pniVerified = false)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpInsert(E164_A, PNI_A, ACI_A)
),
changeSet
)
}
@Test
fun fullMatch() {
val result = applyAndAssert(
Input(E164_A, PNI_A, ACI_A),
Update(E164_A, PNI_A, ACI_A),
Output(E164_A, PNI_A, ACI_A)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.id)
),
result.changeSet
)
}
@Test
fun onlyE164Matches() {
val result = applyAndAssert(
Input(E164_A, null, null),
Update(E164_A, PNI_A, ACI_A),
Output(E164_A, PNI_A, ACI_A)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.id),
operations = listOf(
PnpOperation.SetPni(result.id, PNI_A),
PnpOperation.SetAci(result.id, ACI_A)
)
),
result.changeSet
)
}
@Test
fun onlyE164Matches_pniChanges_noAciProvided_existingPniSession() {
val result = applyAndAssert(
Input(E164_A, PNI_B, null, pniSession = true),
Update(E164_A, PNI_A, null),
Output(E164_A, PNI_A, null)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.id),
operations = listOf(
PnpOperation.SetPni(result.id, PNI_A),
PnpOperation.SessionSwitchoverInsert(result.id)
)
),
result.changeSet
)
}
@Test
fun onlyE164Matches_pniChanges_noAciProvided_noPniSession() {
val result = applyAndAssert(
Input(E164_A, PNI_B, null),
Update(E164_A, PNI_A, null),
Output(E164_A, PNI_A, null)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.id),
operations = listOf(
PnpOperation.SetPni(result.id, PNI_A)
)
),
result.changeSet
)
}
@Test
fun e164AndPniMatches_noExistingSession() {
val result = applyAndAssert(
Input(E164_A, PNI_A, null),
Update(E164_A, PNI_A, ACI_A),
Output(E164_A, PNI_A, ACI_A)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.id),
operations = listOf(
PnpOperation.SetAci(result.id, ACI_A)
)
),
result.changeSet
)
}
@Test
fun e164AndPniMatches_existingPniSession() {
val result = applyAndAssert(
Input(E164_A, PNI_A, null, pniSession = true),
Update(E164_A, PNI_A, ACI_A),
Output(E164_A, PNI_A, ACI_A)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.id),
operations = listOf(
PnpOperation.SetAci(result.id, ACI_A),
PnpOperation.SessionSwitchoverInsert(result.id)
)
),
result.changeSet
)
}
@Test
fun e164AndAciMatches() {
val result = applyAndAssert(
Input(E164_A, null, ACI_A),
Update(E164_A, PNI_A, ACI_A),
Output(E164_A, PNI_A, ACI_A)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.id),
operations = listOf(
PnpOperation.SetPni(result.id, PNI_A)
)
),
result.changeSet
)
}
@Test
fun onlyPniMatches_noExistingSession() {
val result = applyAndAssert(
Input(null, PNI_A, null),
Update(E164_A, PNI_A, ACI_A),
Output(E164_A, PNI_A, ACI_A)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.id),
operations = listOf(
PnpOperation.SetE164(result.id, E164_A),
PnpOperation.SetAci(result.id, ACI_A)
)
),
result.changeSet
)
}
@Test
fun onlyPniMatches_existingPniSession() {
val result = applyAndAssert(
Input(null, PNI_A, null, pniSession = true),
Update(E164_A, PNI_A, ACI_A),
Output(E164_A, PNI_A, ACI_A)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.id),
operations = listOf(
PnpOperation.SetE164(result.id, E164_A),
PnpOperation.SetAci(result.id, ACI_A),
PnpOperation.SessionSwitchoverInsert(result.id)
)
),
result.changeSet
)
}
@Test
fun onlyPniMatches_existingPniSession_changeNumber() {
val result = applyAndAssert(
Input(E164_B, PNI_A, null, pniSession = true),
Update(E164_A, PNI_A, ACI_A),
Output(E164_A, PNI_A, ACI_A)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.id),
operations = listOf(
PnpOperation.SetE164(result.id, E164_A),
PnpOperation.SetAci(result.id, ACI_A),
PnpOperation.ChangeNumberInsert(
recipientId = result.id,
oldE164 = E164_B,
newE164 = E164_A
),
PnpOperation.SessionSwitchoverInsert(result.id)
)
),
result.changeSet
)
}
@Test
fun pniAndAciMatches() {
val result = applyAndAssert(
Input(null, PNI_A, ACI_A),
Update(E164_A, PNI_A, ACI_A),
Output(E164_A, PNI_A, ACI_A)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.id),
operations = listOf(
PnpOperation.SetE164(result.id, E164_A),
)
),
result.changeSet
)
}
@Test
fun pniAndAciMatches_changeNumber() {
val result = applyAndAssert(
Input(E164_B, PNI_A, ACI_A),
Update(E164_A, PNI_A, ACI_A),
Output(E164_A, PNI_A, ACI_A)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.id),
operations = listOf(
PnpOperation.SetE164(result.id, E164_A),
PnpOperation.ChangeNumberInsert(
recipientId = result.id,
oldE164 = E164_B,
newE164 = E164_A
)
)
),
result.changeSet
)
}
@Test
fun onlyAciMatches() {
val result = applyAndAssert(
Input(null, null, ACI_A),
Update(E164_A, PNI_A, ACI_A),
Output(E164_A, PNI_A, ACI_A)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.id),
operations = listOf(
PnpOperation.SetE164(result.id, E164_A),
PnpOperation.SetPni(result.id, PNI_A)
)
),
result.changeSet
)
}
@Test
fun onlyAciMatches_changeNumber() {
val result = applyAndAssert(
Input(E164_B, null, ACI_A),
Update(E164_A, PNI_A, ACI_A),
Output(E164_A, PNI_A, ACI_A)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.id),
operations = listOf(
PnpOperation.SetE164(result.id, E164_A),
PnpOperation.SetPni(result.id, PNI_A),
PnpOperation.ChangeNumberInsert(
recipientId = result.id,
oldE164 = E164_B,
newE164 = E164_A
)
)
),
result.changeSet
)
}
@Test
fun merge_e164Only_pniOnly_aciOnly() {
val result = applyAndAssert(
listOf(
Input(E164_A, null, null),
Input(null, PNI_A, null),
Input(null, null, ACI_A)
),
Update(E164_A, PNI_A, ACI_A),
Output(E164_A, PNI_A, ACI_A)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.thirdId),
operations = listOf(
PnpOperation.Merge(
primaryId = result.firstId,
secondaryId = result.secondId
),
PnpOperation.Merge(
primaryId = result.thirdId,
secondaryId = result.firstId
)
)
),
result.changeSet
)
}
@Test
fun merge_e164Only_pniOnly_noAciProvided() {
val result = applyAndAssert(
listOf(
Input(E164_A, null, null),
Input(null, PNI_A, null),
),
Update(E164_A, PNI_A, null),
Output(E164_A, PNI_A, null)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.firstId),
operations = listOf(
PnpOperation.Merge(
primaryId = result.firstId,
secondaryId = result.secondId
)
)
),
result.changeSet
)
}
@Test
fun merge_e164Only_pniOnly_aciProvidedButNoAciRecord() {
val result = applyAndAssert(
listOf(
Input(E164_A, null, null),
Input(null, PNI_A, null),
),
Update(E164_A, PNI_A, ACI_A),
Output(E164_A, PNI_A, ACI_A)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.firstId),
operations = listOf(
PnpOperation.Merge(
primaryId = result.firstId,
secondaryId = result.secondId
),
PnpOperation.SetAci(
recipientId = result.firstId,
aci = ACI_A
)
)
),
result.changeSet
)
}
@Test
fun merge_e164Only_pniAndE164_noAciProvided() {
val result = applyAndAssert(
listOf(
Input(E164_A, null, null),
Input(E164_B, PNI_A, null),
),
Update(E164_A, PNI_A, null),
Output(E164_A, PNI_A, null)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.firstId),
operations = listOf(
PnpOperation.RemovePni(result.secondId),
PnpOperation.SetPni(
recipientId = result.firstId,
pni = PNI_A
),
)
),
result.changeSet
)
}
@Test
fun merge_e164AndPni_pniOnly_noAciProvided() {
val result = applyAndAssert(
listOf(
Input(E164_A, PNI_B, null),
Input(null, PNI_A, null),
),
Update(E164_A, PNI_A, null),
Output(E164_A, PNI_A, null)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.firstId),
operations = listOf(
PnpOperation.RemovePni(result.firstId),
PnpOperation.Merge(
primaryId = result.firstId,
secondaryId = result.secondId
),
)
),
result.changeSet
)
}
@Test
fun merge_e164AndPni_e164AndPni_noAciProvided_noSessions() {
val result = applyAndAssert(
listOf(
Input(E164_A, PNI_B, null),
Input(E164_B, PNI_A, null),
),
Update(E164_A, PNI_A, null),
Output(E164_A, PNI_A, null)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.firstId),
operations = listOf(
PnpOperation.RemovePni(result.secondId),
PnpOperation.SetPni(result.firstId, PNI_A)
)
),
result.changeSet
)
}
@Test
fun merge_e164AndPni_e164AndPni_noAciProvided_sessionsExist() {
val result = applyAndAssert(
listOf(
Input(E164_A, PNI_B, null, pniSession = true),
Input(E164_B, PNI_A, null, pniSession = true),
),
Update(E164_A, PNI_A, null),
Output(E164_A, PNI_A, null)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.firstId),
operations = listOf(
PnpOperation.RemovePni(result.secondId),
PnpOperation.SetPni(result.firstId, PNI_A),
PnpOperation.SessionSwitchoverInsert(result.secondId),
PnpOperation.SessionSwitchoverInsert(result.firstId)
)
),
result.changeSet
)
}
@Test
fun merge_e164AndPni_aciOnly() {
val result = applyAndAssert(
listOf(
Input(E164_A, PNI_A, null),
Input(null, null, ACI_A),
),
Update(E164_A, PNI_A, ACI_A),
Output(E164_A, PNI_A, ACI_A)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.secondId),
operations = listOf(
PnpOperation.Merge(
primaryId = result.secondId,
secondaryId = result.firstId
),
)
),
result.changeSet
)
}
@Test
fun merge_e164AndPni_aciOnly_e164RecordHasSeparateE164() {
val result = applyAndAssert(
listOf(
Input(E164_B, PNI_A, null),
Input(null, null, ACI_A),
),
Update(E164_A, PNI_A, ACI_A),
Output(E164_A, PNI_A, ACI_A)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.secondId),
operations = listOf(
PnpOperation.RemovePni(result.firstId),
PnpOperation.Update(
recipientId = result.secondId,
e164 = E164_A,
pni = PNI_A,
aci = ACI_A
)
)
),
result.changeSet
)
}
@Test
fun merge_e164AndPni_e164AndPniAndAci_changeNumber() {
val result = applyAndAssert(
listOf(
Input(E164_A, PNI_A, null),
Input(E164_B, PNI_B, ACI_A),
),
Update(E164_A, PNI_A, ACI_A),
Output(E164_A, PNI_A, ACI_A)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.secondId),
operations = listOf(
PnpOperation.RemovePni(result.secondId),
PnpOperation.RemoveE164(result.secondId),
PnpOperation.Merge(
primaryId = result.secondId,
secondaryId = result.firstId
),
PnpOperation.ChangeNumberInsert(
recipientId = result.secondId,
oldE164 = E164_B,
newE164 = E164_A
)
)
),
result.changeSet
)
}
@Test
fun merge_e164AndPni_e164Aci_changeNumber() {
val result = applyAndAssert(
listOf(
Input(E164_A, PNI_A, null),
Input(E164_B, null, ACI_A),
),
Update(E164_A, PNI_A, ACI_A),
Output(E164_A, PNI_A, ACI_A)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.secondId),
operations = listOf(
PnpOperation.RemoveE164(result.secondId),
PnpOperation.Merge(
primaryId = result.secondId,
secondaryId = result.firstId
),
PnpOperation.ChangeNumberInsert(
recipientId = result.secondId,
oldE164 = E164_B,
newE164 = E164_A
)
)
),
result.changeSet
)
}
private fun insert(e164: String?, pni: PNI?, aci: ACI?): RecipientId {
val id: Long = SignalDatabase.rawDatabase.insert(
RecipientDatabase.TABLE_NAME,
null,
contentValuesOf(
RecipientDatabase.PHONE to e164,
RecipientDatabase.SERVICE_ID to (aci ?: pni)?.toString(),
RecipientDatabase.PNI_COLUMN to pni?.toString(),
RecipientDatabase.REGISTERED to RecipientDatabase.RegisteredState.REGISTERED.id
)
)
return RecipientId.from(id)
}
private fun insertMockSessionFor(account: ServiceId, address: ServiceId) {
SignalDatabase.rawDatabase.insert(
SessionDatabase.TABLE_NAME, null,
contentValuesOf(
SessionDatabase.ACCOUNT_ID to account.toString(),
SessionDatabase.ADDRESS to address.toString(),
SessionDatabase.DEVICE to 1,
SessionDatabase.RECORD to Util.getSecretBytes(32)
)
)
}
data class Input(val e164: String?, val pni: PNI?, val aci: ACI?, val pniSession: Boolean = false, val aciSession: Boolean = false)
data class Update(val e164: String?, val pni: PNI?, val aci: ACI?, val pniVerified: Boolean = false)
data class Output(val e164: String?, val pni: PNI?, val aci: ACI?)
data class PnpMatchResult(val ids: List<RecipientId>, val changeSet: PnpChangeSet) {
val id
get() = if (ids.size == 1) {
ids[0]
} else {
throw IllegalStateException("There are multiple IDs, but you assumed 1!")
}
val firstId
get() = ids[0]
val secondId
get() = ids[1]
val thirdId
get() = ids[2]
}
private fun applyAndAssert(input: Input, update: Update, output: Output): PnpMatchResult {
return applyAndAssert(listOf(input), update, output)
}
/**
* Helper method that will call insert your recipients, call [RecipientDatabase.processPnpTupleToChangeSet] with your params,
* and then verify your output matches what you expect.
*
* It results the inserted ID's and changeset for additional verification.
*
* But basically this is here to make the tests more readable. It gives you a clear list of:
* - input
* - update
* - output
*
* that you can spot check easily.
*
* Important: The output will only include records that contain fields from the input. That means
* for:
*
* Input: E164_B, PNI_A, null
* Update: E164_A, PNI_A, null
*
* You will get:
* Output: E164_A, PNI_A, null
*
* Even though there was an update that will also result in the row (E164_B, null, null)
*/
private fun applyAndAssert(input: List<Input>, update: Update, output: Output): PnpMatchResult {
val ids = input.map { insert(it.e164, it.pni, it.aci) }
input
.filter { it.pniSession }
.forEach { insertMockSessionFor(databaseRule.localAci, it.pni!!) }
input
.filter { it.aciSession }
.forEach { insertMockSessionFor(databaseRule.localAci, it.aci!!) }
val byE164 = update.e164?.let { db.getByE164(it).orElse(null) }
val byPniSid = update.pni?.let { db.getByServiceId(it).orElse(null) }
val byAciSid = update.aci?.let { db.getByServiceId(it).orElse(null) }
val data = PnpDataSet(
e164 = update.e164,
pni = update.pni,
aci = update.aci,
byE164 = byE164,
byPniSid = byPniSid,
byPniOnly = update.pni?.let { db.getByPni(it).orElse(null) },
byAciSid = byAciSid,
e164Record = byE164?.let { db.getRecord(it) },
pniSidRecord = byPniSid?.let { db.getRecord(it) },
aciSidRecord = byAciSid?.let { db.getRecord(it) }
)
val changeSet = db.processPnpTupleToChangeSet(update.e164, update.pni, update.aci, pniVerified = update.pniVerified)
val finalData = data.perform(changeSet.operations)
val finalRecords = setOfNotNull(finalData.e164Record, finalData.pniSidRecord, finalData.aciSidRecord)
assertEquals("There's still multiple records in the resulting record set! $finalRecords", 1, finalRecords.size)
finalRecords.firstOrNull { record -> record.e164 == output.e164 && record.pni == output.pni && record.serviceId == (output.aci ?: output.pni) }
?: throw AssertionError("Expected output was not found in the result set! Expected: $output")
return PnpMatchResult(
ids = ids,
changeSet = changeSet
)
}
companion object {
val ACI_A = ACI.from(UUID.fromString("3436efbe-5a76-47fa-a98a-7e72c948a82e"))
val ACI_B = ACI.from(UUID.fromString("8de7f691-0b60-4a68-9cd9-ed2f8453f9ed"))
val PNI_A = PNI.from(UUID.fromString("154b8d92-c960-4f6c-8385-671ad2ffb999"))
val PNI_B = PNI.from(UUID.fromString("ba92b1fb-cd55-40bf-adda-c35a85375533"))
const val E164_A = "+12221234567"
const val E164_B = "+13331234567"
}
}

View File

@@ -1,183 +0,0 @@
package org.thoughtcrime.securesms.database
import androidx.test.ext.junit.runners.AndroidJUnit4
import junit.framework.Assert.assertFalse
import junit.framework.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.signal.core.util.concurrent.SignalExecutors
import java.util.concurrent.CountDownLatch
import java.util.concurrent.atomic.AtomicBoolean
/**
* These are tests for the wrapper we wrote around SQLCipherDatabase, not the stock or SQLCipher one.
*/
@RunWith(AndroidJUnit4::class)
class SQLiteDatabaseTest {
private lateinit var db: SQLiteDatabase
@Before
fun setup() {
db = SignalDatabase.instance!!.signalWritableDatabase
}
@Test
fun runPostSuccessfulTransaction_runsImmediatelyIfNotInTransaction() {
val hasRun = AtomicBoolean(false)
db.runPostSuccessfulTransaction { hasRun.set(true) }
assertTrue(hasRun.get())
}
@Test
fun runPostSuccessfulTransaction_runsAfterSuccessIfInTransaction() {
val hasRun = AtomicBoolean(false)
db.beginTransaction()
db.runPostSuccessfulTransaction { hasRun.set(true) }
assertFalse(hasRun.get())
db.setTransactionSuccessful()
db.endTransaction()
assertTrue(hasRun.get())
}
@Test
fun runPostSuccessfulTransaction_doesNotRunAfterFailedTransaction() {
val hasRun = AtomicBoolean(false)
db.beginTransaction()
db.runPostSuccessfulTransaction { hasRun.set(true) }
assertFalse(hasRun.get())
db.endTransaction()
assertFalse(hasRun.get())
// Verifying we still don't run it even after a subsequent success
db.beginTransaction()
db.setTransactionSuccessful()
db.endTransaction()
assertFalse(hasRun.get())
}
@Test
fun runPostSuccessfulTransaction_onlyRunAfterAllTransactionsComplete() {
val hasRun = AtomicBoolean(false)
db.beginTransaction()
db.runPostSuccessfulTransaction { hasRun.set(true) }
assertFalse(hasRun.get())
db.beginTransaction()
db.setTransactionSuccessful()
db.endTransaction()
assertFalse(hasRun.get())
db.setTransactionSuccessful()
db.endTransaction()
assertTrue(hasRun.get())
}
@Test
fun runPostSuccessfulTransaction_runsImmediatelyIfTheTransactionIsOnAnotherThread() {
db.beginTransaction()
val latch = CountDownLatch(1)
SignalExecutors.BOUNDED.execute {
val hasRun = AtomicBoolean(false)
db.runPostSuccessfulTransaction { hasRun.set(true) }
assertTrue(hasRun.get())
latch.countDown()
}
latch.await()
db.setTransactionSuccessful()
db.endTransaction()
}
@Test
fun runPostSuccessfulTransaction_runsAfterSuccessIfInTransaction_ignoreDuplicates() {
val hasRun1 = AtomicBoolean(false)
val hasRun2 = AtomicBoolean(false)
db.beginTransaction()
db.runPostSuccessfulTransaction("key") { hasRun1.set(true) }
db.runPostSuccessfulTransaction("key") { hasRun2.set(true) }
assertFalse(hasRun1.get())
assertFalse(hasRun2.get())
db.setTransactionSuccessful()
db.endTransaction()
assertTrue(hasRun1.get())
assertFalse(hasRun2.get())
}
@Test
fun runPostSuccessfulTransaction_runsAndPerformsAnotherTransaction() {
val hasRun = AtomicBoolean(false)
db.beginTransaction()
db.runPostSuccessfulTransaction {
try {
db.beginTransaction()
hasRun.set(true)
db.setTransactionSuccessful()
} finally {
db.endTransaction()
}
}
assertFalse(hasRun.get())
db.setTransactionSuccessful()
db.endTransaction()
assertTrue(hasRun.get())
}
@Test
fun runPostSuccessfulTransaction_runsAndPerformsAnotherTransactionAndRunPostNested() {
val hasRun1 = AtomicBoolean(false)
val hasRun2 = AtomicBoolean(false)
db.beginTransaction()
db.runPostSuccessfulTransaction {
db.beginTransaction()
db.runPostSuccessfulTransaction {
assertTrue(hasRun1.get())
assertFalse(hasRun2.get())
hasRun2.set(true)
}
assertFalse(hasRun1.get())
hasRun1.set(true)
assertFalse(hasRun2.get())
db.setTransactionSuccessful()
db.endTransaction()
}
assertFalse(hasRun1.get())
assertFalse(hasRun2.get())
db.setTransactionSuccessful()
db.endTransaction()
assertTrue(hasRun1.get())
assertTrue(hasRun2.get())
}
}

View File

@@ -1,292 +0,0 @@
package org.thoughtcrime.securesms.database
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.`is`
import org.hamcrest.Matchers.notNullValue
import org.hamcrest.Matchers.nullValue
import org.junit.Before
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.databaseprotos.DecryptedGroupV2Context
import org.thoughtcrime.securesms.database.model.databaseprotos.addMember
import org.thoughtcrime.securesms.database.model.databaseprotos.addRequestingMember
import org.thoughtcrime.securesms.database.model.databaseprotos.deleteRequestingMember
import org.thoughtcrime.securesms.database.model.databaseprotos.groupChange
import org.thoughtcrime.securesms.database.model.databaseprotos.groupContext
import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.sms.IncomingGroupUpdateMessage
import org.thoughtcrime.securesms.sms.IncomingTextMessage
import org.whispersystems.signalservice.api.push.ACI
import org.whispersystems.signalservice.api.push.PNI
import org.whispersystems.signalservice.api.push.ServiceId
import java.util.Optional
import java.util.UUID
@Suppress("ClassName", "TestFunctionName")
@RunWith(AndroidJUnit4::class)
class SmsDatabaseTest_collapseJoinRequestEventsIfPossible {
private lateinit var recipients: RecipientDatabase
private lateinit var sms: SmsDatabase
private val localAci = ACI.from(UUID.randomUUID())
private val localPni = PNI.from(UUID.randomUUID())
private var wallClock: Long = 1000
private lateinit var alice: RecipientId
private lateinit var bob: RecipientId
@Before
fun setUp() {
recipients = SignalDatabase.recipients
sms = SignalDatabase.sms
SignalStore.account().setAci(localAci)
SignalStore.account().setPni(localPni)
alice = recipients.getOrInsertFromServiceId(aliceServiceId)
bob = recipients.getOrInsertFromServiceId(bobServiceId)
}
/**
* Do nothing if no previous messages.
*/
@Test
fun noPreviousMessage() {
val result = sms.collapseJoinRequestEventsIfPossible(
1,
groupUpdateMessage(
sender = alice,
groupContext = groupContext(masterKey = masterKey) {
change = groupChange(editor = aliceServiceId) {
deleteRequestingMember(aliceServiceId)
}
}
)
)
assertThat("result is null when not collapsing", result.orElse(null), nullValue())
}
/**
* Do nothing if previous message is text.
*/
@Test
fun previousTextMesssage() {
val threadId = sms.insertMessageInbox(smsMessage(sender = alice, body = "What up")).get().threadId
val result = sms.collapseJoinRequestEventsIfPossible(
threadId,
groupUpdateMessage(
sender = alice,
groupContext = groupContext(masterKey = masterKey) {
change = groupChange(editor = aliceServiceId) {
deleteRequestingMember(aliceServiceId)
}
}
)
)
assertThat("result is null when not collapsing", result.orElse(null), nullValue())
}
/**
* Do nothing if previous is unrelated group change.
*/
@Test
fun previousUnrelatedGroupChange() {
val threadId = sms.insertMessageInbox(
groupUpdateMessage(
sender = alice,
groupContext = groupContext(masterKey = masterKey) {
change = groupChange(editor = aliceServiceId) {
addMember(bobServiceId)
}
}
)
).get().threadId
val result = sms.collapseJoinRequestEventsIfPossible(
threadId,
groupUpdateMessage(
sender = alice,
groupContext = groupContext(masterKey = masterKey) {
change = groupChange(editor = aliceServiceId) {
deleteRequestingMember(aliceServiceId)
}
}
)
)
assertThat("result is null when not collapsing", result.orElse(null), nullValue())
}
/**
* Do nothing if previous join request is from a different recipient.
*/
@Test
fun previousJoinRequestFromADifferentRecipient() {
val threadId = sms.insertMessageInbox(
groupUpdateMessage(
sender = bob,
groupContext = groupContext(masterKey = masterKey) {
change = groupChange(editor = bobServiceId) {
deleteRequestingMember(bobServiceId)
}
}
)
).get().threadId
val result = sms.collapseJoinRequestEventsIfPossible(
threadId,
groupUpdateMessage(
sender = alice,
groupContext = groupContext(masterKey = masterKey) {
change = groupChange(editor = aliceServiceId) {
deleteRequestingMember(aliceServiceId)
}
}
)
)
assertThat("result is null when not collapsing", result.orElse(null), nullValue())
}
/**
* Collapse if previous is join request from same.
*/
@Test
fun previousJoinRequestCollapse() {
val latestMessage: MessageDatabase.InsertResult = sms.insertMessageInbox(
groupUpdateMessage(
sender = alice,
groupContext = groupContext(masterKey = masterKey) {
change = groupChange(editor = aliceServiceId) {
addRequestingMember(aliceServiceId)
}
}
)
).get()
val result = sms.collapseJoinRequestEventsIfPossible(
latestMessage.threadId,
groupUpdateMessage(
sender = alice,
groupContext = groupContext(masterKey = masterKey) {
change = groupChange(editor = aliceServiceId) {
deleteRequestingMember(aliceServiceId)
}
}
)
)
assertThat("result is not null when collapsing", result.orElse(null), notNullValue())
assertThat("result message id should be same as latest message", result.get().messageId, `is`(latestMessage.messageId))
}
/**
* Collapse if previous is join request from same, and leave second previous alone if text.
*/
@Test
fun previousJoinThenTextCollapse() {
val secondLatestMessage = sms.insertMessageInbox(smsMessage(sender = alice, body = "What up")).get()
val latestMessage: MessageDatabase.InsertResult = sms.insertMessageInbox(
groupUpdateMessage(
sender = alice,
groupContext = groupContext(masterKey = masterKey) {
change = groupChange(editor = aliceServiceId) {
addRequestingMember(aliceServiceId)
}
}
)
).get()
assert(secondLatestMessage.threadId == latestMessage.threadId)
val result = sms.collapseJoinRequestEventsIfPossible(
latestMessage.threadId,
groupUpdateMessage(
sender = alice,
groupContext = groupContext(masterKey = masterKey) {
change = groupChange(editor = aliceServiceId) {
deleteRequestingMember(aliceServiceId)
}
}
)
)
assertThat("result is not null when collapsing", result.orElse(null), notNullValue())
assertThat("result message id should be same as latest message", result.get().messageId, `is`(latestMessage.messageId))
}
/**
* Collapse "twice" is previous is a join request and second previous is already collapsed join/delete from the same recipient.
*/
@Test
fun previousCollapseAndJoinRequestDoubleCollapse() {
val secondLatestMessage: MessageDatabase.InsertResult = sms.insertMessageInbox(
groupUpdateMessage(
sender = alice,
groupContext = groupContext(masterKey = masterKey) {
change = groupChange(editor = aliceServiceId) {
addRequestingMember(aliceServiceId)
deleteRequestingMember(aliceServiceId)
}
}
)
).get()
val latestMessage: MessageDatabase.InsertResult = sms.insertMessageInbox(
groupUpdateMessage(
sender = alice,
groupContext = groupContext(masterKey = masterKey) {
change = groupChange(editor = aliceServiceId) {
addRequestingMember(aliceServiceId)
}
}
)
).get()
assert(secondLatestMessage.threadId == latestMessage.threadId)
val result = sms.collapseJoinRequestEventsIfPossible(
latestMessage.threadId,
groupUpdateMessage(
sender = alice,
groupContext = groupContext(masterKey = masterKey) {
change = groupChange(editor = aliceServiceId) {
deleteRequestingMember(aliceServiceId)
}
}
)
)
assertThat("result is not null when collapsing", result.orElse(null), notNullValue())
assertThat("result message id should be same as second latest message", result.get().messageId, `is`(secondLatestMessage.messageId))
assertThat("latest message should be deleted", sms.getMessageRecordOrNull(latestMessage.messageId), nullValue())
}
private fun smsMessage(sender: RecipientId, body: String? = ""): IncomingTextMessage {
wallClock++
return IncomingTextMessage(sender, 1, wallClock, wallClock, wallClock, body, Optional.of(groupId), 0, true, null)
}
private fun groupUpdateMessage(sender: RecipientId, groupContext: DecryptedGroupV2Context): IncomingGroupUpdateMessage {
return IncomingGroupUpdateMessage(smsMessage(sender, null), groupContext)
}
companion object {
private val aliceServiceId: ServiceId = ACI.from(UUID.fromString("3436efbe-5a76-47fa-a98a-7e72c948a82e"))
private val bobServiceId: ServiceId = ACI.from(UUID.fromString("8de7f691-0b60-4a68-9cd9-ed2f8453f9ed"))
private val masterKey = GroupMasterKey(Hex.fromStringCondensed("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"))
private val groupId = GroupId.v2(masterKey)
}
}

View File

@@ -1,463 +0,0 @@
package org.thoughtcrime.securesms.database
import androidx.test.ext.junit.runners.AndroidJUnit4
import junit.framework.TestCase.assertNull
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.containsInAnyOrder
import org.hamcrest.Matchers.hasSize
import org.hamcrest.Matchers.`is`
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.thoughtcrime.securesms.database.model.DistributionListId
import org.thoughtcrime.securesms.database.model.StoryType
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.whispersystems.signalservice.api.push.DistributionId
import org.whispersystems.signalservice.api.push.ServiceId
import java.util.UUID
@RunWith(AndroidJUnit4::class)
class StorySendsDatabaseTest {
private val distributionId1 = DistributionId.from(UUID.randomUUID())
private val distributionId2 = DistributionId.from(UUID.randomUUID())
private val distributionId3 = DistributionId.from(UUID.randomUUID())
private lateinit var distributionList1: DistributionListId
private lateinit var distributionList2: DistributionListId
private lateinit var distributionList3: DistributionListId
private lateinit var distributionListRecipient1: Recipient
private lateinit var distributionListRecipient2: Recipient
private lateinit var distributionListRecipient3: Recipient
private lateinit var recipients1to10: List<RecipientId>
private lateinit var recipients11to20: List<RecipientId>
private lateinit var recipients6to15: List<RecipientId>
private lateinit var recipients6to10: List<RecipientId>
private var messageId1: Long = 0
private var messageId2: Long = 0
private var messageId3: Long = 0
private lateinit var storySends: StorySendsDatabase
@Before
fun setup() {
storySends = SignalDatabase.storySends
recipients1to10 = makeRecipients(10)
recipients11to20 = makeRecipients(10)
distributionList1 = SignalDatabase.distributionLists.createList("1", emptyList(), distributionId = distributionId1)!!
distributionList2 = SignalDatabase.distributionLists.createList("2", emptyList(), distributionId = distributionId2)!!
distributionList3 = SignalDatabase.distributionLists.createList("3", emptyList(), distributionId = distributionId3)!!
distributionListRecipient1 = Recipient.resolved(SignalDatabase.recipients.getOrInsertFromDistributionListId(distributionList1))
distributionListRecipient2 = Recipient.resolved(SignalDatabase.recipients.getOrInsertFromDistributionListId(distributionList2))
distributionListRecipient3 = Recipient.resolved(SignalDatabase.recipients.getOrInsertFromDistributionListId(distributionList3))
messageId1 = MmsHelper.insert(
recipient = distributionListRecipient1,
storyType = StoryType.STORY_WITHOUT_REPLIES,
)
messageId2 = MmsHelper.insert(
recipient = distributionListRecipient2,
storyType = StoryType.STORY_WITH_REPLIES,
)
messageId3 = MmsHelper.insert(
recipient = distributionListRecipient3,
storyType = StoryType.STORY_WITHOUT_REPLIES,
)
recipients6to15 = recipients1to10.takeLast(5) + recipients11to20.take(5)
recipients6to10 = recipients1to10.takeLast(5)
}
@Test
fun getRecipientsToSendTo_noOverlap() {
storySends.insert(messageId1, recipients1to10, 100, false, distributionId1)
storySends.insert(messageId2, recipients11to20, 200, true, distributionId2)
storySends.insert(messageId3, recipients1to10, 300, false, distributionId3)
val recipientIdsForMessage1 = storySends.getRecipientsToSendTo(messageId1, 100, false)
val recipientIdsForMessage2 = storySends.getRecipientsToSendTo(messageId2, 200, true)
assertThat(recipientIdsForMessage1, hasSize(10))
assertThat(recipientIdsForMessage1, containsInAnyOrder(*recipients1to10.toTypedArray()))
assertThat(recipientIdsForMessage2, hasSize(10))
assertThat(recipientIdsForMessage2, containsInAnyOrder(*recipients11to20.toTypedArray()))
}
@Test
fun getRecipientsToSendTo_overlap() {
storySends.insert(messageId1, recipients1to10, 100, false, distributionId1)
storySends.insert(messageId2, recipients6to15, 100, true, distributionId2)
val recipientIdsForMessage1 = storySends.getRecipientsToSendTo(messageId1, 100, false)
val recipientIdsForMessage2 = storySends.getRecipientsToSendTo(messageId2, 100, true)
assertThat(recipientIdsForMessage1, hasSize(5))
assertThat(recipientIdsForMessage1, containsInAnyOrder(*recipients1to10.take(5).toTypedArray()))
assertThat(recipientIdsForMessage2, hasSize(10))
assertThat(recipientIdsForMessage2, containsInAnyOrder(*recipients6to15.toTypedArray()))
}
@Test
fun getRecipientsToSendTo_overlapAll() {
val recipient1 = recipients1to10.first()
val recipient2 = recipients11to20.first()
storySends.insert(messageId1, listOf(recipient1, recipient2), 100, false, distributionId1)
storySends.insert(messageId2, listOf(recipient1), 100, true, distributionId2)
storySends.insert(messageId3, listOf(recipient2), 100, true, distributionId3)
val recipientIdsForMessage1 = storySends.getRecipientsToSendTo(messageId1, 100, false)
val recipientIdsForMessage2 = storySends.getRecipientsToSendTo(messageId2, 100, true)
val recipientIdsForMessage3 = storySends.getRecipientsToSendTo(messageId3, 100, true)
assertThat(recipientIdsForMessage1, hasSize(0))
assertThat(recipientIdsForMessage2, hasSize(1))
assertThat(recipientIdsForMessage2, containsInAnyOrder(recipient1))
assertThat(recipientIdsForMessage3, hasSize(1))
assertThat(recipientIdsForMessage3, containsInAnyOrder(recipient2))
}
@Test
fun getRecipientsToSendTo_overlapWithEarlierMessage() {
storySends.insert(messageId1, recipients6to15, 100, true, distributionId1)
storySends.insert(messageId2, recipients1to10, 100, false, distributionId2)
val recipientIdsForMessage1 = storySends.getRecipientsToSendTo(messageId1, 100, true)
val recipientIdsForMessage2 = storySends.getRecipientsToSendTo(messageId2, 100, false)
assertThat(recipientIdsForMessage1, hasSize(10))
assertThat(recipientIdsForMessage1, containsInAnyOrder(*recipients6to15.toTypedArray()))
assertThat(recipientIdsForMessage2, hasSize(5))
assertThat(recipientIdsForMessage2, containsInAnyOrder(*recipients1to10.take(5).toTypedArray()))
}
@Test
fun getRemoteDeleteRecipients_noOverlap() {
storySends.insert(messageId1, recipients1to10, 100, false, distributionId1)
storySends.insert(messageId2, recipients11to20, 200, true, distributionId2)
storySends.insert(messageId3, recipients1to10, 300, false, distributionId3)
val recipientIdsForMessage1 = storySends.getRemoteDeleteRecipients(messageId1, 100)
val recipientIdsForMessage2 = storySends.getRemoteDeleteRecipients(messageId2, 200)
assertThat(recipientIdsForMessage1, hasSize(10))
assertThat(recipientIdsForMessage1, containsInAnyOrder(*recipients1to10.toTypedArray()))
assertThat(recipientIdsForMessage2, hasSize(10))
assertThat(recipientIdsForMessage2, containsInAnyOrder(*recipients11to20.toTypedArray()))
}
@Test
fun getRemoteDeleteRecipients_overlapNoPreviousDeletes() {
storySends.insert(messageId1, recipients1to10, 200, false, distributionId1)
storySends.insert(messageId2, recipients6to15, 200, true, distributionId2)
val recipientIdsForMessage1 = storySends.getRemoteDeleteRecipients(messageId1, 200)
val recipientIdsForMessage2 = storySends.getRemoteDeleteRecipients(messageId2, 200)
assertThat(recipientIdsForMessage1, hasSize(5))
assertThat(recipientIdsForMessage1, containsInAnyOrder(*recipients1to10.take(5).toTypedArray()))
assertThat(recipientIdsForMessage2, hasSize(5))
assertThat(recipientIdsForMessage2, containsInAnyOrder(*recipients6to15.takeLast(5).toTypedArray()))
}
@Test
fun getRemoteDeleteRecipients_overlapWithPreviousDeletes() {
storySends.insert(messageId1, recipients1to10, 200, false, distributionId1)
SignalDatabase.mms.markAsRemoteDelete(messageId1)
storySends.insert(messageId2, recipients6to15, 200, true, distributionId2)
val recipientIdsForMessage2 = storySends.getRemoteDeleteRecipients(messageId2, 200)
assertThat(recipientIdsForMessage2, hasSize(10))
assertThat(recipientIdsForMessage2, containsInAnyOrder(*recipients6to15.toTypedArray()))
}
@Test
fun canReply_storyWithReplies() {
storySends.insert(messageId2, recipients1to10, 200, true, distributionId2)
val canReply = storySends.canReply(recipients1to10[0], 200)
assertThat(canReply, `is`(true))
}
@Test
fun canReply_storyWithoutReplies() {
storySends.insert(messageId1, recipients1to10, 200, false, distributionId1)
val canReply = storySends.canReply(recipients1to10[0], 200)
assertThat(canReply, `is`(false))
}
@Test
fun canReply_storyWithAndWithoutRepliesOverlap() {
storySends.insert(messageId1, recipients1to10, 200, false, distributionId1)
storySends.insert(messageId2, recipients6to10, 200, true, distributionId2)
val message1OnlyRecipientCanReply = storySends.canReply(recipients1to10[0], 200)
val message2RecipientCanReply = storySends.canReply(recipients6to10[0], 200)
assertThat(message1OnlyRecipientCanReply, `is`(false))
assertThat(message2RecipientCanReply, `is`(true))
}
@Test
fun givenASingleStory_whenIGetFullSentStorySyncManifest_thenIExpectNotNull() {
storySends.insert(messageId1, recipients1to10, 200, false, distributionId1)
val manifest = storySends.getFullSentStorySyncManifest(messageId1, 200)
assertNotNull(manifest)
}
@Test
fun givenTwoStories_whenIGetFullSentStorySyncManifestForStory2_thenIExpectNull() {
storySends.insert(messageId1, recipients1to10, 200, false, distributionId1)
storySends.insert(messageId2, recipients1to10, 200, false, distributionId2)
val manifest = storySends.getFullSentStorySyncManifest(messageId2, 200)
assertNull(manifest)
}
@Test
fun givenTwoStories_whenIGetFullSentStorySyncManifestForStory1_thenIExpectOneManifestPerRecipient() {
storySends.insert(messageId1, recipients1to10, 200, false, distributionId1)
storySends.insert(messageId2, recipients1to10, 200, true, distributionId2)
val manifest = storySends.getFullSentStorySyncManifest(messageId1, 200)!!
assertEquals(recipients1to10, manifest.entries.map { it.recipientId })
}
@Test
fun givenTwoStories_whenIGetFullSentStorySyncManifestForStory1_thenIExpectTwoListsPerRecipient() {
storySends.insert(messageId1, recipients1to10, 200, false, distributionId1)
storySends.insert(messageId2, recipients1to10, 200, true, distributionId2)
val manifest = storySends.getFullSentStorySyncManifest(messageId1, 200)!!
manifest.entries.forEach { entry ->
assertEquals(listOf(distributionId1, distributionId2), entry.distributionLists)
}
}
@Test
fun givenTwoStories_whenIGetFullSentStorySyncManifestForStory1_thenIExpectAllRecipientsCanReply() {
storySends.insert(messageId1, recipients1to10, 200, false, distributionId1)
storySends.insert(messageId2, recipients1to10, 200, true, distributionId2)
val manifest = storySends.getFullSentStorySyncManifest(messageId1, 200)!!
manifest.entries.forEach { entry ->
assertTrue(entry.allowedToReply)
}
}
@Test
fun givenTwoStoriesAndOneIsRemoteDeleted_whenIGetFullSentStorySyncManifestForStory2_thenIExpectNonNullResult() {
storySends.insert(messageId1, recipients1to10, 200, false, distributionId1)
storySends.insert(messageId2, recipients1to10, 200, true, distributionId2)
SignalDatabase.mms.markAsRemoteDelete(messageId1)
val manifest = storySends.getFullSentStorySyncManifest(messageId2, 200)!!
assertNotNull(manifest)
}
@Test
fun givenTwoStoriesAndOneIsRemoteDeleted_whenIGetRecipientIdsForManifestUpdate_thenIExpectOnlyRecipientsWithStory2() {
storySends.insert(messageId1, recipients1to10, 200, false, distributionId1)
storySends.insert(messageId1, recipients11to20, 200, false, distributionId1)
storySends.insert(messageId2, recipients1to10, 200, true, distributionId2)
SignalDatabase.mms.markAsRemoteDelete(messageId1)
val recipientIds = storySends.getRecipientIdsForManifestUpdate(200, messageId1)
assertEquals(recipients1to10.toHashSet(), recipientIds)
}
@Test
fun givenTwoStoriesAndOneIsRemoteDeleted_whenIGetPartialSentStorySyncManifest_thenIExpectOnlyRecipientsThatHadStory1() {
storySends.insert(messageId1, recipients1to10, 200, false, distributionId1)
storySends.insert(messageId2, recipients1to10, 200, true, distributionId2)
storySends.insert(messageId2, recipients11to20, 200, true, distributionId2)
SignalDatabase.mms.markAsRemoteDelete(messageId1)
val recipientIds = storySends.getRecipientIdsForManifestUpdate(200, messageId1)
val results = storySends.getSentStorySyncManifestForUpdate(200, recipientIds)
val manifestRecipients = results.entries.map { it.recipientId }
assertEquals(recipients1to10, manifestRecipients)
}
@Test
fun givenTwoStoriesAndTheOneThatAllowedRepliesIsRemoteDeleted_whenIGetPartialSentStorySyncManifest_thenIExpectAllowRepliesToBeTrue() {
storySends.insert(messageId1, recipients1to10, 200, false, distributionId1)
storySends.insert(messageId2, recipients1to10, 200, true, distributionId2)
SignalDatabase.mms.markAsRemoteDelete(messageId2)
val recipientIds = storySends.getRecipientIdsForManifestUpdate(200, messageId1)
val results = storySends.getSentStorySyncManifestForUpdate(200, recipientIds)
assertTrue(results.entries.all { it.allowedToReply })
}
@Test
fun givenEmptyManifest_whenIApplyRemoteManifest_thenNothingChanges() {
storySends.insert(messageId1, recipients1to10, 200, false, distributionId1)
val expected = storySends.getFullSentStorySyncManifest(messageId1, 200)
val emptyManifest = SentStorySyncManifest(emptyList())
storySends.applySentStoryManifest(emptyManifest, 200)
val result = storySends.getFullSentStorySyncManifest(messageId1, 200)
assertEquals(expected, result)
}
@Test
fun givenAnIdenticalManifest_whenIApplyRemoteManifest_thenNothingChanges() {
val messageId4 = MmsHelper.insert(
recipient = distributionListRecipient1,
storyType = StoryType.STORY_WITHOUT_REPLIES,
sentTimeMillis = 200
)
storySends.insert(messageId4, recipients1to10, 200, false, distributionId1)
val expected = storySends.getFullSentStorySyncManifest(messageId4, 200)
storySends.applySentStoryManifest(expected!!, 200)
val result = storySends.getFullSentStorySyncManifest(messageId4, 200)
assertEquals(expected, result)
}
@Test
fun givenAManifest_whenIApplyRemoteManifestWithoutOneList_thenIExpectMessageToBeMarkedRemoteDeleted() {
val messageId4 = MmsHelper.insert(
recipient = distributionListRecipient1,
storyType = StoryType.STORY_WITHOUT_REPLIES,
sentTimeMillis = 200
)
val messageId5 = MmsHelper.insert(
recipient = distributionListRecipient2,
storyType = StoryType.STORY_WITHOUT_REPLIES,
sentTimeMillis = 200
)
storySends.insert(messageId4, recipients1to10, 200, false, distributionId1)
val remote = storySends.getFullSentStorySyncManifest(messageId4, 200)!!
storySends.insert(messageId5, recipients1to10, 200, false, distributionId2)
storySends.applySentStoryManifest(remote, 200)
assertTrue(SignalDatabase.mms.getMessageRecord(messageId5).isRemoteDelete)
}
@Test
fun givenAManifest_whenIApplyRemoteManifestWithoutOneList_thenIExpectSharedMessageToNotBeMarkedRemoteDeleted() {
val messageId4 = MmsHelper.insert(
recipient = distributionListRecipient1,
storyType = StoryType.STORY_WITHOUT_REPLIES,
sentTimeMillis = 200
)
val messageId5 = MmsHelper.insert(
recipient = distributionListRecipient2,
storyType = StoryType.STORY_WITHOUT_REPLIES,
sentTimeMillis = 200
)
storySends.insert(messageId4, recipients1to10, 200, false, distributionId1)
val remote = storySends.getFullSentStorySyncManifest(messageId4, 200)!!
storySends.insert(messageId5, recipients1to10, 200, false, distributionId2)
storySends.applySentStoryManifest(remote, 200)
assertFalse(SignalDatabase.mms.getMessageRecord(messageId4).isRemoteDelete)
}
@Test
fun givenNoLocalEntries_whenIApplyRemoteManifest_thenIExpectLocalManifestToMatch() {
val messageId4 = MmsHelper.insert(
recipient = distributionListRecipient1,
storyType = StoryType.STORY_WITHOUT_REPLIES,
sentTimeMillis = 2000
)
val remote = SentStorySyncManifest(
recipients1to10.map {
SentStorySyncManifest.Entry(
recipientId = it,
allowedToReply = true,
distributionLists = listOf(distributionId1)
)
}
)
storySends.applySentStoryManifest(remote, 2000)
val local = storySends.getFullSentStorySyncManifest(messageId4, 2000)
assertEquals(remote, local)
}
@Test
fun givenNonStoryMessageAtSentTimestamp_whenIApplyRemoteManifest_thenIExpectLocalManifestToMatchAndNoCrashes() {
val messageId4 = MmsHelper.insert(
recipient = distributionListRecipient1,
storyType = StoryType.STORY_WITHOUT_REPLIES,
sentTimeMillis = 2000
)
MmsHelper.insert(
recipient = Recipient.resolved(recipients1to10.first()),
sentTimeMillis = 2000
)
val remote = SentStorySyncManifest(
recipients1to10.map {
SentStorySyncManifest.Entry(
recipientId = it,
allowedToReply = true,
distributionLists = listOf(distributionId1)
)
}
)
storySends.applySentStoryManifest(remote, 2000)
val local = storySends.getFullSentStorySyncManifest(messageId4, 2000)
assertEquals(remote, local)
}
private fun makeRecipients(count: Int): List<RecipientId> {
return (1..count).map {
SignalDatabase.recipients.getOrInsertFromServiceId(ServiceId.from(UUID.randomUUID()))
}
}
}

View File

@@ -1,74 +0,0 @@
package org.thoughtcrime.securesms.database
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.signal.core.util.CursorUtil
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.testing.SignalDatabaseRule
import org.whispersystems.signalservice.api.push.ServiceId
import java.util.UUID
@Suppress("ClassName")
class ThreadDatabaseTest_pinned {
@Rule
@JvmField
val databaseRule = SignalDatabaseRule()
private lateinit var recipient: Recipient
@Before
fun setUp() {
recipient = Recipient.resolved(SignalDatabase.recipients.getOrInsertFromServiceId(ServiceId.from(UUID.randomUUID())))
}
@Test
fun givenAPinnedThread_whenIDeleteTheLastMessage_thenIDoNotDeleteOrUnpinTheThread() {
// GIVEN
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(recipient)
val messageId = MmsHelper.insert(recipient = recipient, threadId = threadId)
SignalDatabase.threads.pinConversations(listOf(threadId))
// WHEN
SignalDatabase.mms.deleteMessage(messageId)
// THEN
val pinned = SignalDatabase.threads.pinnedThreadIds
assertTrue(threadId in pinned)
}
@Test
fun givenAPinnedThread_whenIDeleteTheLastMessage_thenIExpectTheThreadInUnarchivedCount() {
// GIVEN
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(recipient)
val messageId = MmsHelper.insert(recipient = recipient, threadId = threadId)
SignalDatabase.threads.pinConversations(listOf(threadId))
// WHEN
SignalDatabase.mms.deleteMessage(messageId)
// THEN
val unarchivedCount = SignalDatabase.threads.unarchivedConversationListCount
assertEquals(1, unarchivedCount)
}
@Test
fun givenAPinnedThread_whenIDeleteTheLastMessage_thenIExpectPinnedThreadInUnarchivedList() {
// GIVEN
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(recipient)
val messageId = MmsHelper.insert(recipient = recipient, threadId = threadId)
SignalDatabase.threads.pinConversations(listOf(threadId))
// WHEN
SignalDatabase.mms.deleteMessage(messageId)
// THEN
SignalDatabase.threads.getUnarchivedConversationList(true, 0, 1).use {
it.moveToFirst()
assertEquals(threadId, CursorUtil.requireLong(it, ThreadDatabase.ID))
}
}
}

View File

@@ -1,10 +1,7 @@
package org.thoughtcrime.securesms.lock;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.signal.core.util.Hex;
import org.thoughtcrime.securesms.util.Hex;
import org.whispersystems.signalservice.api.kbs.HashedPin;
import org.whispersystems.signalservice.api.kbs.KbsData;
import org.whispersystems.signalservice.api.kbs.MasterKey;
@@ -15,7 +12,6 @@ import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
@RunWith(AndroidJUnit4.class)
public final class PinHashing_hashPin_Test {
@Test

View File

@@ -1,139 +0,0 @@
package org.thoughtcrime.securesms.safety
import io.reactivex.rxjava3.plugins.RxJavaPlugins
import io.reactivex.rxjava3.schedulers.TestScheduler
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.testing.SignalActivityRule
class SafetyNumberBottomSheetRepositoryTest {
@get:Rule val harness = SignalActivityRule(othersCount = 10)
private val testScheduler = TestScheduler()
private val subjectUnderTest = SafetyNumberBottomSheetRepository()
@Before
fun setUp() {
RxJavaPlugins.setInitIoSchedulerHandler { testScheduler }
RxJavaPlugins.setIoSchedulerHandler { testScheduler }
}
@Test
fun givenIOnlyHave1to1Destinations_whenIGetBuckets_thenIOnlyHaveContactsBucketContainingAllRecipients() {
val recipients = harness.others
val destinations = harness.others.map { ContactSearchKey.RecipientSearchKey.KnownRecipient(it) }
val result = subjectUnderTest.getBuckets(recipients, destinations).test()
testScheduler.triggerActions()
result.assertValueAt(1) { map ->
assertMatch(map, mapOf(SafetyNumberBucket.ContactsBucket to harness.others))
}
}
@Test
fun givenIOnlyHaveASingle1to1Destination_whenIGetBuckets_thenIOnlyHaveContactsBucketContainingAllRecipients() {
// GIVEN
val recipients = harness.others
val destination = harness.others.take(1).map { ContactSearchKey.RecipientSearchKey.KnownRecipient(it) }
// WHEN
val result = subjectUnderTest.getBuckets(recipients, destination).test(1)
testScheduler.triggerActions()
// THEN
result.assertValue { map ->
assertMatch(map, mapOf(SafetyNumberBucket.ContactsBucket to harness.others.take(1)))
}
}
@Test
fun givenIHaveADistributionListDestination_whenIGetBuckets_thenIOnlyHaveDistributionListDestinationWithCorrespondingMembers() {
// GIVEN
val distributionListMembers = harness.others.take(5)
val distributionList = SignalDatabase.distributionLists.createList("ListA", distributionListMembers)!!
val destinationKey = ContactSearchKey.RecipientSearchKey.Story(SignalDatabase.distributionLists.getRecipientId(distributionList)!!)
// WHEN
val result = subjectUnderTest.getBuckets(harness.others, listOf(destinationKey)).test(1)
testScheduler.triggerActions()
// THEN
result.assertValue { map ->
assertMatch(
map,
mapOf(
SafetyNumberBucket.DistributionListBucket(distributionList, "ListA") to harness.others.take(5)
)
)
}
}
@Test
fun givenIHaveADistributionListDestinationAndIGetBuckets_whenIRemoveFromStories_thenIOnlyHaveDistributionListDestinationWithCorrespondingMembers() {
// GIVEN
val distributionListMembers = harness.others.take(5)
val toRemove = distributionListMembers.last()
val distributionList = SignalDatabase.distributionLists.createList("ListA", distributionListMembers)!!
val destinationKey = ContactSearchKey.RecipientSearchKey.Story(SignalDatabase.distributionLists.getRecipientId(distributionList)!!)
val testSubscriber = subjectUnderTest.getBuckets(distributionListMembers, listOf(destinationKey)).test(2)
testScheduler.triggerActions()
// WHEN
subjectUnderTest.removeFromStories(toRemove, listOf(destinationKey)).subscribe()
testSubscriber.request(1)
testScheduler.triggerActions()
testSubscriber.awaitCount(3)
// THEN
testSubscriber.assertValueAt(2) { map ->
assertMatch(
map,
mapOf(
SafetyNumberBucket.DistributionListBucket(distributionList, "ListA") to distributionListMembers.dropLast(1)
)
)
}
}
@Test
fun givenIHaveADistributionListDestinationAndIGetBuckets_whenIRemoveAllFromStory_thenINoLongerHaveEntryForThatBucket() {
// GIVEN
val distributionListMembers = harness.others.take(5)
val distributionList = SignalDatabase.distributionLists.createList("ListA", distributionListMembers)!!
val destinationKey = ContactSearchKey.RecipientSearchKey.Story(SignalDatabase.distributionLists.getRecipientId(distributionList)!!)
val testSubscriber = subjectUnderTest.getBuckets(distributionListMembers, listOf(destinationKey)).test(2)
testScheduler.triggerActions()
// WHEN
subjectUnderTest.removeAllFromStory(distributionListMembers, distributionList).subscribe()
testSubscriber.request(1)
testScheduler.triggerActions()
testSubscriber.awaitCount(3)
// THEN
testSubscriber.assertValueAt(2) { map ->
assertMatch(map, mapOf())
}
}
private fun assertMatch(
resultMap: Map<SafetyNumberBucket, List<SafetyNumberRecipient>>,
idMap: Map<SafetyNumberBucket, List<RecipientId>>
): Boolean {
assertEquals("Result and ID Maps had different key sets", idMap.keys, resultMap.keys)
resultMap.forEach { (bucket, members) ->
assertEquals("Mismatch in Bucket $bucket", idMap[bucket], members.map { it.recipient.id })
}
return true
}
}

View File

@@ -1,125 +0,0 @@
package org.thoughtcrime.securesms.testing
import android.app.Activity
import android.app.Application
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.preference.PreferenceManager
import androidx.test.core.app.ActivityScenario
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.rules.ExternalResource
import org.signal.libsignal.protocol.IdentityKey
import org.signal.libsignal.protocol.SignalProtocolAddress
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
import org.thoughtcrime.securesms.crypto.MasterSecretUtil
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil
import org.thoughtcrime.securesms.database.IdentityDatabase
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.net.DeviceTransferBlockingInterceptor
import org.thoughtcrime.securesms.profiles.ProfileName
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.registration.RegistrationData
import org.thoughtcrime.securesms.registration.RegistrationRepository
import org.thoughtcrime.securesms.registration.RegistrationUtil
import org.thoughtcrime.securesms.util.Util
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile
import org.whispersystems.signalservice.api.push.ACI
import org.whispersystems.signalservice.api.push.SignalServiceAddress
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse
import java.lang.IllegalArgumentException
import java.util.UUID
/**
* Test rule to use that sets up the application in a mostly registered state. Enough so that most
* activities should be launchable directly.
*
* To use: `@get:Rule val harness = SignalActivityRule()`
*/
class SignalActivityRule(private val othersCount: Int = 4) : ExternalResource() {
val application: Application = ApplicationDependencies.getApplication()
lateinit var context: Context
private set
lateinit var self: Recipient
private set
lateinit var others: List<RecipientId>
private set
override fun before() {
context = InstrumentationRegistry.getInstrumentation().targetContext
self = setupSelf()
others = setupOthers()
}
private fun setupSelf(): Recipient {
DeviceTransferBlockingInterceptor.getInstance().blockNetwork()
PreferenceManager.getDefaultSharedPreferences(application).edit().putBoolean("pref_prompted_push_registration", true).commit()
val masterSecret = MasterSecretUtil.generateMasterSecret(application, MasterSecretUtil.UNENCRYPTED_PASSPHRASE)
MasterSecretUtil.generateAsymmetricMasterSecret(application, masterSecret)
val preferences: SharedPreferences = application.getSharedPreferences(MasterSecretUtil.PREFERENCES_NAME, 0)
preferences.edit().putBoolean("passphrase_initialized", true).commit()
val registrationRepository = RegistrationRepository(application)
registrationRepository.registerAccountWithoutRegistrationLock(
RegistrationData(
code = "123123",
e164 = "+15554045550101",
password = Util.getSecret(18),
registrationId = registrationRepository.registrationId,
profileKey = registrationRepository.getProfileKey("+15554045550101"),
fcmToken = null
),
VerifyAccountResponse(UUID.randomUUID().toString(), UUID.randomUUID().toString(), false)
).blockingGet()
SignalStore.kbsValues().optOut()
RegistrationUtil.maybeMarkRegistrationComplete(application)
SignalDatabase.recipients.setProfileName(Recipient.self().id, ProfileName.fromParts("Tester", "McTesterson"))
return Recipient.self()
}
private fun setupOthers(): List<RecipientId> {
val others = mutableListOf<RecipientId>()
if (othersCount !in 0 until 1000) {
throw IllegalArgumentException("$othersCount must be between 0 and 1000")
}
for (i in 0 until othersCount) {
val aci = ACI.from(UUID.randomUUID())
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, true, true, true, true, true))
SignalDatabase.recipients.setProfileSharing(recipientId, true)
ApplicationDependencies.getProtocolStore().aci().saveIdentity(SignalProtocolAddress(aci.toString(), 0), IdentityKeyUtil.generateIdentityKeyPair().publicKey)
others += recipientId
}
return others
}
inline fun <reified T : Activity> launchActivity(initIntent: Intent.() -> Unit): ActivityScenario<T> {
return androidx.test.core.app.launchActivity(Intent(context, T::class.java).apply(initIntent))
}
fun changeIdentityKey(recipient: Recipient, identityKey: IdentityKey = IdentityKeyUtil.generateIdentityKeyPair().publicKey) {
ApplicationDependencies.getProtocolStore().aci().saveIdentity(SignalProtocolAddress(recipient.requireServiceId().toString(), 0), identityKey)
}
fun getIdentity(recipient: Recipient): IdentityKey {
return ApplicationDependencies.getProtocolStore().aci().identities().getIdentity(SignalProtocolAddress(recipient.requireServiceId().toString(), 0))
}
fun setVerified(recipient: Recipient, status: IdentityDatabase.VerifiedStatus) {
ApplicationDependencies.getProtocolStore().aci().identities().setVerified(recipient.id, getIdentity(recipient), IdentityDatabase.VerifiedStatus.VERIFIED)
}
}

View File

@@ -1,40 +0,0 @@
package org.thoughtcrime.securesms.testing
import org.junit.rules.TestWatcher
import org.junit.runner.Description
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.whispersystems.signalservice.api.push.ACI
import org.whispersystems.signalservice.api.push.PNI
import java.util.UUID
/**
* Sets up bare-minimum to allow writing unit tests against the database,
* including setting up the local ACI and PNI pair.
*
* @param deleteAllThreadsOnEachRun Run deleteAllThreads between each unit test
*/
class SignalDatabaseRule(
private val deleteAllThreadsOnEachRun: Boolean = true
) : TestWatcher() {
val localAci: ACI = ACI.from(UUID.randomUUID())
val localPni: PNI = PNI.from(UUID.randomUUID())
override fun starting(description: Description?) {
deleteAllThreads()
SignalStore.account().setAci(localAci)
SignalStore.account().setPni(localPni)
}
override fun finished(description: Description?) {
deleteAllThreads()
}
private fun deleteAllThreads() {
if (deleteAllThreadsOnEachRun) {
SignalDatabase.mms.deleteAllThreads()
}
}
}

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="org.thoughtcrime.securesms">
<application
android:name=".FlipperApplicationContext"
tools:replace="android:name">
<activity
android:name="com.facebook.flipper.android.diagnostics.FlipperDiagnosticActivity"
android:exported="true" />
</application>
</manifest>

View File

@@ -0,0 +1,27 @@
package org.thoughtcrime.securesms;
import com.facebook.flipper.android.AndroidFlipperClient;
import com.facebook.flipper.core.FlipperClient;
import com.facebook.flipper.plugins.databases.DatabasesFlipperPlugin;
import com.facebook.flipper.plugins.inspector.DescriptorMapping;
import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin;
import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin;
import com.facebook.soloader.SoLoader;
import org.thoughtcrime.securesms.database.FlipperSqlCipherAdapter;
public class FlipperApplicationContext extends ApplicationContext {
@Override
public void onCreate() {
super.onCreate();
SoLoader.init(this, false);
FlipperClient client = AndroidFlipperClient.getInstance(this);
client.addPlugin(new InspectorFlipperPlugin(this, DescriptorMapping.withDefaults()));
client.addPlugin(new DatabasesFlipperPlugin(new FlipperSqlCipherAdapter(this)));
client.addPlugin(new SharedPreferencesFlipperPlugin(this));
client.start();
}
}

View File

@@ -0,0 +1,275 @@
package org.thoughtcrime.securesms.database;
import android.app.Application;
import android.content.Context;
import android.database.Cursor;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.facebook.flipper.plugins.databases.DatabaseDescriptor;
import com.facebook.flipper.plugins.databases.DatabaseDriver;
import net.zetetic.database.DatabaseUtils;
import net.zetetic.database.sqlcipher.SQLiteDatabase;
import net.zetetic.database.sqlcipher.SQLiteStatement;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import org.thoughtcrime.securesms.util.Hex;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
* A lot of this code is taken from {@link com.facebook.flipper.plugins.databases.impl.SqliteDatabaseDriver}
* and made to work with SqlCipher. Unfortunately I couldn't use it directly, nor subclass it.
*/
public class FlipperSqlCipherAdapter extends DatabaseDriver<FlipperSqlCipherAdapter.Descriptor> {
private static final String TAG = Log.tag(FlipperSqlCipherAdapter.class);
public FlipperSqlCipherAdapter(Context context) {
super(context);
}
@Override
public List<Descriptor> getDatabases() {
try {
Field databaseHelperField = DatabaseFactory.class.getDeclaredField("databaseHelper");
databaseHelperField.setAccessible(true);
SignalDatabase mainOpenHelper = Objects.requireNonNull((SQLCipherOpenHelper) databaseHelperField.get(DatabaseFactory.getInstance(getContext())));
SignalDatabase keyValueOpenHelper = KeyValueDatabase.getInstance((Application) getContext());
SignalDatabase megaphoneOpenHelper = MegaphoneDatabase.getInstance((Application) getContext());
SignalDatabase jobManagerOpenHelper = JobDatabase.getInstance((Application) getContext());
SignalDatabase metricsOpenHelper = LocalMetricsDatabase.getInstance((Application) getContext());
return Arrays.asList(new Descriptor(mainOpenHelper),
new Descriptor(keyValueOpenHelper),
new Descriptor(megaphoneOpenHelper),
new Descriptor(jobManagerOpenHelper),
new Descriptor(metricsOpenHelper));
} catch (Exception e) {
Log.i(TAG, "Unable to use reflection to access raw database.", e);
}
return Collections.emptyList();
}
@Override
public List<String> getTableNames(Descriptor descriptor) {
SQLiteDatabase db = descriptor.getReadable();
List<String> tableNames = new ArrayList<>();
try (Cursor cursor = db.rawQuery("SELECT name FROM sqlite_master WHERE type IN (?, ?)", new String[] { "table", "view" })) {
while (cursor != null && cursor.moveToNext()) {
tableNames.add(cursor.getString(0));
}
}
return tableNames;
}
@Override
public DatabaseGetTableDataResponse getTableData(Descriptor descriptor, String table, String order, boolean reverse, int start, int count) {
SQLiteDatabase db = descriptor.getReadable();
long total = DatabaseUtils.queryNumEntries(db, table);
String orderBy = order != null ? order + (reverse ? " DESC" : " ASC") : null;
String limitBy = start + ", " + count;
try (Cursor cursor = db.query(table, null, null, null, null, null, orderBy, limitBy)) {
String[] columnNames = cursor.getColumnNames();
List<List<Object>> rows = cursorToList(cursor);
return new DatabaseGetTableDataResponse(Arrays.asList(columnNames), rows, start, rows.size(), total);
}
}
@Override
public DatabaseGetTableStructureResponse getTableStructure(Descriptor descriptor, String table) {
SQLiteDatabase db = descriptor.getReadable();
Map<String, String> foreignKeyValues = new HashMap<>();
try(Cursor cursor = db.rawQuery("PRAGMA foreign_key_list(" + table + ")", null)) {
while (cursor != null && cursor.moveToNext()) {
String from = cursor.getString(cursor.getColumnIndex("from"));
String to = cursor.getString(cursor.getColumnIndex("to"));
String tableName = cursor.getString(cursor.getColumnIndex("table")) + "(" + to + ")";
foreignKeyValues.put(from, tableName);
}
}
List<String> structureColumns = Arrays.asList("column_name", "data_type", "nullable", "default", "primary_key", "foreign_key");
List<List<Object>> structureValues = new ArrayList<>();
try (Cursor cursor = db.rawQuery("PRAGMA table_info(" + table + ")", null)) {
while (cursor != null && cursor.moveToNext()) {
String columnName = cursor.getString(cursor.getColumnIndex("name"));
String foreignKey = foreignKeyValues.containsKey(columnName) ? foreignKeyValues.get(columnName) : null;
structureValues.add(Arrays.asList(columnName,
cursor.getString(cursor.getColumnIndex("type")),
cursor.getInt(cursor.getColumnIndex("notnull")) == 0,
getObjectFromColumnIndex(cursor, cursor.getColumnIndex("dflt_value")),
cursor.getInt(cursor.getColumnIndex("pk")) == 1,
foreignKey));
}
}
List<String> indexesColumns = Arrays.asList("index_name", "unique", "indexed_column_name");
List<List<Object>> indexesValues = new ArrayList<>();
try (Cursor indexesCursor = db.rawQuery("PRAGMA index_list(" + table + ")", null)) {
List<String> indexedColumnNames = new ArrayList<>();
String indexName = indexesCursor.getString(indexesCursor.getColumnIndex("name"));
try(Cursor indexInfoCursor = db.rawQuery("PRAGMA index_info(" + indexName + ")", null)) {
while (indexInfoCursor.moveToNext()) {
indexedColumnNames.add(indexInfoCursor.getString(indexInfoCursor.getColumnIndex("name")));
}
}
indexesValues.add(Arrays.asList(indexName,
indexesCursor.getInt(indexesCursor.getColumnIndex("unique")) == 1,
TextUtils.join(",", indexedColumnNames)));
}
return new DatabaseGetTableStructureResponse(structureColumns, structureValues, indexesColumns, indexesValues);
}
@Override
public DatabaseGetTableInfoResponse getTableInfo(Descriptor databaseDescriptor, String table) {
SQLiteDatabase db = databaseDescriptor.getReadable();
try (Cursor cursor = db.rawQuery("SELECT sql FROM sqlite_master WHERE name = ?", new String[] { table })) {
cursor.moveToFirst();
return new DatabaseGetTableInfoResponse(cursor.getString(cursor.getColumnIndex("sql")));
}
}
@Override
public DatabaseExecuteSqlResponse executeSQL(Descriptor descriptor, String query) {
SQLiteDatabase db = descriptor.getWritable();
String firstWordUpperCase = getFirstWord(query).toUpperCase();
switch (firstWordUpperCase) {
case "UPDATE":
case "DELETE":
return executeUpdateDelete(db, query);
case "INSERT":
return executeInsert(db, query);
case "SELECT":
case "PRAGMA":
case "EXPLAIN":
return executeSelect(db, query);
default:
return executeRawQuery(db, query);
}
}
private static String getFirstWord(String s) {
s = s.trim();
int firstSpace = s.indexOf(' ');
return firstSpace >= 0 ? s.substring(0, firstSpace) : s;
}
private static DatabaseExecuteSqlResponse executeUpdateDelete(SQLiteDatabase database, String query) {
SQLiteStatement statement = database.compileStatement(query);
int count = statement.executeUpdateDelete();
return DatabaseExecuteSqlResponse.successfulUpdateDelete(count);
}
private static DatabaseExecuteSqlResponse executeInsert(SQLiteDatabase database, String query) {
SQLiteStatement statement = database.compileStatement(query);
long insertedId = statement.executeInsert();
return DatabaseExecuteSqlResponse.successfulInsert(insertedId);
}
private static DatabaseExecuteSqlResponse executeSelect(SQLiteDatabase database, String query) {
try (Cursor cursor = database.rawQuery(query, null)) {
String[] columnNames = cursor.getColumnNames();
List<List<Object>> rows = cursorToList(cursor);
return DatabaseExecuteSqlResponse.successfulSelect(Arrays.asList(columnNames), rows);
}
}
private static DatabaseExecuteSqlResponse executeRawQuery(SQLiteDatabase database, String query) {
database.execSQL(query);
return DatabaseExecuteSqlResponse.successfulRawQuery();
}
private static @NonNull List<List<Object>> cursorToList(Cursor cursor) {
List<List<Object>> rows = new ArrayList<>();
int numColumns = cursor.getColumnCount();
while (cursor.moveToNext()) {
List<Object> values = new ArrayList<>(numColumns);
for (int column = 0; column < numColumns; column++) {
values.add(getObjectFromColumnIndex(cursor, column));
}
rows.add(values);
}
return rows;
}
private static @Nullable Object getObjectFromColumnIndex(Cursor cursor, int column) {
switch (cursor.getType(column)) {
case Cursor.FIELD_TYPE_NULL:
return null;
case Cursor.FIELD_TYPE_INTEGER:
return cursor.getLong(column);
case Cursor.FIELD_TYPE_FLOAT:
return cursor.getDouble(column);
case Cursor.FIELD_TYPE_BLOB:
byte[] blob = cursor.getBlob(column);
String bytes = blob != null ? "(blob) " + Hex.toStringCondensed(Arrays.copyOf(blob, Math.min(blob.length, 32))) : null;
if (bytes != null && bytes.length() == 32 && blob.length > 32) {
bytes += "...";
}
return bytes;
case Cursor.FIELD_TYPE_STRING:
default:
return cursor.getString(column);
}
}
static class Descriptor implements DatabaseDescriptor {
private final SignalDatabase sqlCipherOpenHelper;
Descriptor(@NonNull SignalDatabase sqlCipherOpenHelper) {
this.sqlCipherOpenHelper = sqlCipherOpenHelper;
}
@Override
public String name() {
return sqlCipherOpenHelper.getDatabaseName();
}
public @NonNull SQLiteDatabase getReadable() {
return sqlCipherOpenHelper.getSqlCipherDatabase();
}
public @NonNull SQLiteDatabase getWritable() {
return sqlCipherOpenHelper.getSqlCipherDatabase();
}
}
}

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<background android:drawable="@color/core_red_shade"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

View File

@@ -91,8 +91,6 @@
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT"/>
<uses-permission android:name="android.permission.MANAGE_OWN_CALLS"/>
<application android:name=".ApplicationContext"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
@@ -103,10 +101,6 @@
android:theme="@style/TextSecure.LightTheme"
android:largeHeap="true">
<meta-data
android:name="com.google.android.gms.wallet.api.enabled"
android:value="true" />
<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="AIzaSyCSx9xea86GwDKGznCAULE9Y5a8b-TfN9U"/>
@@ -179,12 +173,11 @@
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:windowSoftInputMode="adjustResize" />
<activity android:name=".sharing.v2.ShareActivity"
<activity android:name=".sharing.ShareActivity"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:excludeFromRecents="true"
android:taskAffinity=""
android:windowSoftInputMode="stateHidden"
android:exported="true"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize">
<intent-filter>
<action android:name="android.intent.action.SEND" />
@@ -224,7 +217,7 @@
<data android:scheme="sgnl"
android:host="addstickers" />
</intent-filter>
<intent-filter android:autoVerify="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
@@ -258,7 +251,7 @@
android:noHistory="true"
android:theme="@style/Signal.Transparent">
<intent-filter android:autoVerify="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
@@ -275,7 +268,7 @@
android:host="signal.group"/>
</intent-filter>
<intent-filter android:autoVerify="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
@@ -285,7 +278,7 @@
android:host="signal.tube" />
</intent-filter>
<intent-filter android:autoVerify="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
@@ -311,6 +304,8 @@
android:allowEmbedded="true"
android:resizeableActivity="true" />
<activity android:name=".longmessage.LongMessageActivity" />
<activity android:name=".conversation.ConversationPopupActivity"
android:windowSoftInputMode="stateVisible"
android:launchMode="singleTask"
@@ -319,6 +314,12 @@
android:theme="@style/TextSecure.LightTheme.Popup"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" />
<activity android:name=".messagedetails.MessageDetailsActivity"
android:windowSoftInputMode="stateHidden"
android:launchMode="singleTask"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:theme="@style/Signal.DayNight.NoActionBar" />
<activity android:name=".groups.ui.invitesandrequests.ManagePendingAndRequestingMembersActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"
android:theme="@style/Theme.Signal.DayNight.NoActionBar" />
@@ -375,7 +376,7 @@
android:label="@string/AndroidManifest__change_passphrase"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".verify.VerifyIdentityActivity"
<activity android:name=".VerifyIdentityActivity"
android:theme="@style/Signal.DayNight.NoActionBar"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
@@ -389,47 +390,16 @@
</intent-filter>
</activity>
<activity
android:name=".stories.my.MyStoriesActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:theme="@style/Signal.DayNight.NoActionBar"
android:windowSoftInputMode="stateAlwaysHidden" />
<activity
android:name=".stories.settings.StorySettingsActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:theme="@style/Signal.DayNight.NoActionBar"
android:windowSoftInputMode="stateAlwaysHidden|adjustResize" />
<activity
android:name=".stories.viewer.StoryViewerActivity"
android:screenOrientation="portrait"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:theme="@style/TextSecure.DarkNoActionBar.StoryViewer"
android:launchMode="singleTask"
android:windowSoftInputMode="stateAlwaysHidden|adjustNothing"
android:parentActivityName=".MainActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="org.thoughtcrime.securesms.MainActivity" />
</activity>
<activity android:name=".components.settings.app.changenumber.ChangeNumberLockActivity"
android:theme="@style/Signal.DayNight.NoActionBar"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".components.settings.conversation.ConversationSettingsActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|screenLayout|screenSize"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:theme="@style/Signal.DayNight.ConversationSettings"
android:windowSoftInputMode="stateAlwaysHidden">
</activity>
<activity android:name=".badges.gifts.flow.GiftFlowActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|screenLayout|screenSize"
android:theme="@style/Signal.DayNight.NoActionBar"
android:windowSoftInputMode="stateAlwaysHidden">
</activity>
<activity android:name=".wallpaper.ChatWallpaperActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
@@ -454,7 +424,7 @@
<activity android:name=".registration.RegistrationNavigationActivity"
android:launchMode="singleTask"
android:theme="@style/TextSecure.LightRegistrationTheme"
android:windowSoftInputMode="stateHidden"
android:windowSoftInputMode="stateUnchanged"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".revealable.ViewOnceMessageActivity"
@@ -471,7 +441,6 @@
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".DeviceActivity"
android:screenOrientation="portrait"
android:label="@string/AndroidManifest__linked_devices"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
@@ -549,7 +518,6 @@
<activity android:name=".blocked.BlockedUsersActivity"
android:theme="@style/TextSecure.LightTheme"
android:windowSoftInputMode="stateHidden"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".scribbles.ImageEditorStickerSelectActivity"
@@ -660,17 +628,10 @@
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<service android:enabled="true" android:name=".service.webrtc.WebRtcCallService" android:foregroundServiceType="camera|microphone"/>
<service android:enabled="true" android:name=".service.webrtc.WebRtcCallService"/>
<service android:enabled="true" android:name=".service.ApplicationMigrationService"/>
<service android:enabled="true" android:exported="false" android:name=".service.KeyCachingService"/>
<service android:enabled="true" android:name=".messages.IncomingMessageObserver$ForegroundService"/>
<service android:name=".service.webrtc.AndroidCallConnectionService"
android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE"
android:exported="true">
<intent-filter>
<action android:name="android.telecom.ConnectionService" />
</intent-filter>
</service>
<service android:name=".components.voice.VoiceNotePlaybackService">
<intent-filter>
@@ -714,9 +675,7 @@
<service android:name=".service.GenericForegroundService"/>
<service android:name=".gcm.FcmFetchBackgroundService" />
<service android:name=".gcm.FcmFetchForegroundService" />
<service android:name=".gcm.FcmFetchService" />
<service android:name=".gcm.FcmReceiveService">
<intent-filter>
@@ -775,8 +734,6 @@
<receiver android:name=".service.ExpirationListener" />
<receiver android:name=".service.ExpiringStoriesManager$ExpireStoriesAlarm" />
<receiver android:name=".revealable.ViewOnceMessageManager$ViewOnceAlarm" />
<receiver android:name=".service.PendingRetryReceiptManager$PendingRetryReceiptAlarm" />
@@ -809,6 +766,22 @@
</provider>
<provider android:name=".database.DatabaseContentProviders$Conversation"
android:authorities="${applicationId}.database.conversation"
android:exported="false" />
<provider android:name=".database.DatabaseContentProviders$Attachment"
android:authorities="${applicationId}.database.attachment"
android:exported="false" />
<provider android:name=".database.DatabaseContentProviders$Sticker"
android:authorities="${applicationId}.database.sticker"
android:exported="false" />
<provider android:name=".database.DatabaseContentProviders$StickerPack"
android:authorities="${applicationId}.database.stickerpack"
android:exported="false" />
<receiver android:name=".service.BootReceiver">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 125 KiB

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 166 KiB

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 238 KiB

After

Width:  |  Height:  |  Size: 233 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 176 KiB

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 150 KiB

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 149 KiB

After

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 150 KiB

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 KiB

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 142 KiB

After

Width:  |  Height:  |  Size: 177 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 174 KiB

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 200 KiB

After

Width:  |  Height:  |  Size: 195 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

After

Width:  |  Height:  |  Size: 91 KiB

File diff suppressed because one or more lines are too long

View File

@@ -1,14 +0,0 @@
package com.google.android.material.bottomsheet
import android.view.View
import android.widget.FrameLayout
import java.lang.ref.WeakReference
/**
* Manually adjust the nested scrolling child for a given [BottomSheetBehavior].
*/
object BottomSheetBehaviorHack {
fun setNestedScrollingChild(behavior: BottomSheetBehavior<FrameLayout>, view: View) {
behavior.nestedScrollingChildRef = WeakReference(view)
}
}

View File

@@ -13,13 +13,12 @@ public final class AppCapabilities {
private static final boolean GV1_MIGRATION = true;
private static final boolean ANNOUNCEMENT_GROUPS = true;
private static final boolean SENDER_KEY = true;
private static final boolean CHANGE_NUMBER = true;
/**
* @param storageCapable Whether or not the user can use storage service. This is another way of
* asking if the user has set a Signal PIN or not.
*/
public static AccountAttributes.Capabilities getCapabilities(boolean storageCapable) {
return new AccountAttributes.Capabilities(UUID_CAPABLE, GV2_CAPABLE, storageCapable, GV1_MIGRATION, SENDER_KEY, ANNOUNCEMENT_GROUPS, CHANGE_NUMBER, FeatureFlags.stories(), FeatureFlags.giftBadges());
return new AccountAttributes.Capabilities(UUID_CAPABLE, GV2_CAPABLE, storageCapable, GV1_MIGRATION, SENDER_KEY, ANNOUNCEMENT_GROUPS, FeatureFlags.changeNumber());
}
}

View File

@@ -33,9 +33,10 @@ public final class AppInitialization {
InsightsOptOut.userRequestedOptOut(context);
TextSecurePreferences.setAppMigrationVersion(context, ApplicationMigrations.CURRENT_VERSION);
TextSecurePreferences.setJobManagerVersion(context, JobManager.CURRENT_VERSION);
TextSecurePreferences.setLastVersionCode(context, Util.getCanonicalVersionCode());
TextSecurePreferences.setLastExperienceVersionCode(context, Util.getCanonicalVersionCode());
TextSecurePreferences.setHasSeenStickerIntroTooltip(context, true);
TextSecurePreferences.setPasswordDisabled(context, true);
TextSecurePreferences.setLastExperienceVersionCode(context, Util.getCanonicalVersionCode());
TextSecurePreferences.setReadReceiptsEnabled(context, true);
TextSecurePreferences.setTypingIndicatorsEnabled(context, true);
TextSecurePreferences.setHasSeenWelcomeScreen(context, false);
@@ -52,11 +53,9 @@ public final class AppInitialization {
Log.i(TAG, "onPostBackupRestore()");
ApplicationDependencies.getMegaphoneRepository().onFirstEverAppLaunch();
SignalStore.onPostBackupRestore();
SignalStore.onFirstEverAppLaunch();
SignalStore.onboarding().clearAll();
TextSecurePreferences.onPostBackupRestore(context);
TextSecurePreferences.setPasswordDisabled(context, true);
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.ZOZO.getPackId(), BlessedPacks.ZOZO.getPackKey(), false));
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.BANDIT.getPackId(), BlessedPacks.BANDIT.getPackKey(), false));
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.DAY_BY_DAY.getPackId(), BlessedPacks.DAY_BY_DAY.getPackKey(), false));
@@ -74,9 +73,10 @@ public final class AppInitialization {
InsightsOptOut.userRequestedOptOut(context);
TextSecurePreferences.setAppMigrationVersion(context, ApplicationMigrations.CURRENT_VERSION);
TextSecurePreferences.setJobManagerVersion(context, JobManager.CURRENT_VERSION);
TextSecurePreferences.setLastVersionCode(context, Util.getCanonicalVersionCode());
TextSecurePreferences.setLastExperienceVersionCode(context, Util.getCanonicalVersionCode());
TextSecurePreferences.setHasSeenStickerIntroTooltip(context, true);
TextSecurePreferences.setPasswordDisabled(context, true);
TextSecurePreferences.setLastExperienceVersionCode(context, Util.getCanonicalVersionCode());
ApplicationDependencies.getMegaphoneRepository().onFirstEverAppLaunch();
SignalStore.onFirstEverAppLaunch();
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.ZOZO.getPackId(), BlessedPacks.ZOZO.getPackKey(), false));

View File

@@ -35,45 +35,34 @@ import org.signal.core.util.logging.AndroidLogger;
import org.signal.core.util.logging.Log;
import org.signal.core.util.tracing.Tracer;
import org.signal.glide.SignalGlideCodecs;
import org.signal.libsignal.protocol.logging.SignalProtocolLoggerProvider;
import org.signal.ringrtc.CallManager;
import org.thoughtcrime.securesms.avatar.AvatarPickerStorage;
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider;
import org.thoughtcrime.securesms.crypto.DatabaseSecretProvider;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.LogDatabase;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.database.SqlCipherLibraryLoader;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencyProvider;
import org.thoughtcrime.securesms.emoji.EmojiSource;
import org.thoughtcrime.securesms.emoji.JumboEmoji;
import org.thoughtcrime.securesms.gcm.FcmJobService;
import org.thoughtcrime.securesms.jobs.CheckServiceReachabilityJob;
import org.thoughtcrime.securesms.jobs.CreateSignedPreKeyJob;
import org.thoughtcrime.securesms.jobs.DownloadLatestEmojiDataJob;
import org.thoughtcrime.securesms.jobs.EmojiSearchIndexDownloadJob;
import org.thoughtcrime.securesms.jobs.FcmRefreshJob;
import org.thoughtcrime.securesms.jobs.FontDownloaderJob;
import org.thoughtcrime.securesms.jobs.GroupV2UpdateSelfProfileKeyJob;
import org.thoughtcrime.securesms.jobs.GroupV1MigrationJob;
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
import org.thoughtcrime.securesms.jobs.ProfileUploadJob;
import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob;
import org.thoughtcrime.securesms.jobs.RefreshPreKeysJob;
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
import org.thoughtcrime.securesms.jobs.RetrieveRemoteAnnouncementsJob;
import org.thoughtcrime.securesms.jobs.StoryOnboardingDownloadJob;
import org.thoughtcrime.securesms.jobs.SubscriptionKeepAliveJob;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.logging.CustomSignalProtocolLogger;
import org.thoughtcrime.securesms.logging.PersistentLogger;
import org.thoughtcrime.securesms.messageprocessingalarm.MessageProcessReceiver;
import org.thoughtcrime.securesms.migrations.ApplicationMigrations;
import org.thoughtcrime.securesms.mms.SignalGlideComponents;
import org.thoughtcrime.securesms.mms.SignalGlideModule;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
import org.thoughtcrime.securesms.ratelimit.RateLimitUtil;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.registration.RegistrationUtil;
import org.thoughtcrime.securesms.ringrtc.RingRtcLogger;
import org.thoughtcrime.securesms.service.DirectoryRefreshListener;
@@ -82,7 +71,6 @@ import org.thoughtcrime.securesms.service.LocalBackupListener;
import org.thoughtcrime.securesms.service.RotateSenderCertificateListener;
import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener;
import org.thoughtcrime.securesms.service.UpdateApkRefreshListener;
import org.thoughtcrime.securesms.service.webrtc.AndroidTelecomUtil;
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
import org.thoughtcrime.securesms.util.AppForegroundObserver;
import org.thoughtcrime.securesms.util.AppStartup;
@@ -94,17 +82,15 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.VersionTracker;
import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageContextWrapper;
import org.webrtc.voiceengine.WebRtcAudioManager;
import org.webrtc.voiceengine.WebRtcAudioUtils;
import org.whispersystems.libsignal.logging.SignalProtocolLoggerProvider;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.security.Security;
import java.util.concurrent.TimeUnit;
import io.reactivex.rxjava3.exceptions.OnErrorNotImplementedException;
import io.reactivex.rxjava3.exceptions.UndeliverableException;
import io.reactivex.rxjava3.plugins.RxJavaPlugins;
import io.reactivex.rxjava3.schedulers.Schedulers;
import rxdogtag2.RxDogTag;
/**
* Will be called once when the TextSecure process is created.
@@ -139,18 +125,16 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
super.onCreate();
AppStartup.getInstance().addBlocking("security-provider", this::initializeSecurityProvider)
.addBlocking("sqlcipher-init", () -> {
SqlCipherLibraryLoader.load();
SignalDatabase.init(this,
DatabaseSecretProvider.getOrCreateDatabaseSecret(this),
AttachmentSecretProvider.getInstance(this).getOrCreateAttachmentSecret());
})
.addBlocking("sqlcipher-init", () -> SqlCipherLibraryLoader.load())
.addBlocking("logging", () -> {
initializeLogging();
Log.i(TAG, "onCreate()");
})
.addBlocking("crash-handling", this::initializeCrashHandling)
.addBlocking("rx-init", this::initializeRx)
.addBlocking("rx-init", () -> {
RxJavaPlugins.setInitIoSchedulerHandler(schedulerSupplier -> Schedulers.from(SignalExecutors.BOUNDED_IO, true, false));
RxJavaPlugins.setInitComputationSchedulerHandler(schedulerSupplier -> Schedulers.from(SignalExecutors.BOUNDED, true, false));
})
.addBlocking("event-bus", () -> EventBus.builder().logNoSubscriberMessages(false).installDefaultEventBus())
.addBlocking("app-dependencies", this::initializeAppDependencies)
.addBlocking("notification-channels", () -> NotificationChannels.create(this))
@@ -174,12 +158,11 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
})
.addBlocking("blob-provider", this::initializeBlobProvider)
.addBlocking("feature-flags", FeatureFlags::init)
.addBlocking("glide", () -> SignalGlideModule.setRegisterGlideComponents(new SignalGlideComponents()))
.addNonBlocking(this::cleanAvatarStorage)
.addNonBlocking(this::initializeRevealableMessageManager)
.addNonBlocking(this::initializePendingRetryReceiptManager)
.addNonBlocking(this::initializeFcmCheck)
.addNonBlocking(CreateSignedPreKeyJob::enqueueIfNeeded)
.addNonBlocking(this::initializeGcmCheck)
.addNonBlocking(this::initializeSignedPreKeyCheck)
.addNonBlocking(this::initializePeriodicTasks)
.addNonBlocking(this::initializeCircumvention)
.addNonBlocking(this::initializePendingMessages)
@@ -190,21 +173,12 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
.addNonBlocking(() -> ApplicationDependencies.getJobManager().beginJobLoop())
.addNonBlocking(EmojiSource::refresh)
.addNonBlocking(() -> ApplicationDependencies.getGiphyMp4Cache().onAppStart(this))
.addNonBlocking(this::ensureProfileUploaded)
.addNonBlocking(() -> ApplicationDependencies.getExpireStoriesManager().scheduleIfNecessary())
.addPostRender(() -> RateLimitUtil.retryAllRateLimitedMessages(this))
.addPostRender(this::initializeExpiringMessageManager)
.addPostRender(() -> SignalStore.settings().setDefaultSms(Util.isDefaultSmsProvider(this)))
.addPostRender(() -> DownloadLatestEmojiDataJob.scheduleIfNecessary(this))
.addPostRender(EmojiSearchIndexDownloadJob::scheduleIfNecessary)
.addPostRender(() -> SignalDatabase.messageLog().trimOldMessages(System.currentTimeMillis(), FeatureFlags.retryRespondMaxAge()))
.addPostRender(() -> JumboEmoji.updateCurrentVersion(this))
.addPostRender(RetrieveRemoteAnnouncementsJob::enqueue)
.addPostRender(() -> AndroidTelecomUtil.registerPhoneAccount())
.addPostRender(() -> ApplicationDependencies.getJobManager().add(new FontDownloaderJob()))
.addPostRender(CheckServiceReachabilityJob::enqueueIfNecessary)
.addPostRender(GroupV2UpdateSelfProfileKeyJob::enqueueForGroupsIfNecessary)
.addPostRender(StoryOnboardingDownloadJob.Companion::enqueueIfNeeded)
.addPostRender(() -> DatabaseFactory.getMessageLogDatabase(this).trimOldMessages(System.currentTimeMillis(), FeatureFlags.retryRespondMaxAge()))
.execute();
Log.d(TAG, "onCreate() took " + (System.currentTimeMillis() - startTime) + " ms");
@@ -217,15 +191,14 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
long startTime = System.currentTimeMillis();
Log.i(TAG, "App is now visible.");
ApplicationDependencies.getFrameRateTracker().start();
ApplicationDependencies.getFrameRateTracker().begin();
ApplicationDependencies.getMegaphoneRepository().onAppForegrounded();
ApplicationDependencies.getDeadlockDetector().start();
SubscriptionKeepAliveJob.enqueueAndTrackTimeIfNecessary();
SignalExecutors.BOUNDED.execute(() -> {
FeatureFlags.refreshIfNecessary();
ApplicationDependencies.getRecipientCache().warmUp();
RetrieveProfileJob.enqueueRoutineFetchIfNecessary(this);
GroupV1MigrationJob.enqueueRoutineMigrationsIfNecessary(this);
executePendingContactSync();
KeyCachingService.onAppForegrounded(this);
ApplicationDependencies.getShakeToReport().enable();
@@ -240,9 +213,8 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
Log.i(TAG, "App is no longer visible.");
KeyCachingService.onAppBackgrounded(this);
ApplicationDependencies.getMessageNotifier().clearVisibleThread();
ApplicationDependencies.getFrameRateTracker().stop();
ApplicationDependencies.getFrameRateTracker().end();
ApplicationDependencies.getShakeToReport().disable();
ApplicationDependencies.getDeadlockDetector().stop();
}
public PersistentLogger getPersistentLogger() {
@@ -286,10 +258,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
SignalProtocolLoggerProvider.setProvider(new CustomSignalProtocolLogger());
SignalExecutors.UNBOUNDED.execute(() -> {
Log.blockUntilAllWritesFinished();
LogDatabase.getInstance(this).trimToSize();
});
SignalExecutors.UNBOUNDED.execute(() -> LogDatabase.getInstance(this).trimToSize());
}
private void initializeCrashHandling() {
@@ -297,30 +266,6 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
Thread.setDefaultUncaughtExceptionHandler(new SignalUncaughtExceptionHandler(originalHandler));
}
private void initializeRx() {
RxDogTag.install();
RxJavaPlugins.setInitIoSchedulerHandler(schedulerSupplier -> Schedulers.from(SignalExecutors.BOUNDED_IO, true, false));
RxJavaPlugins.setInitComputationSchedulerHandler(schedulerSupplier -> Schedulers.from(SignalExecutors.BOUNDED, true, false));
RxJavaPlugins.setErrorHandler(e -> {
boolean wasWrapped = false;
while ((e instanceof UndeliverableException || e instanceof AssertionError || e instanceof OnErrorNotImplementedException) && e.getCause() != null) {
wasWrapped = true;
e = e.getCause();
}
if (wasWrapped && (e instanceof SocketException || e instanceof SocketTimeoutException || e instanceof InterruptedException)) {
return;
}
Thread.UncaughtExceptionHandler uncaughtExceptionHandler = Thread.currentThread().getUncaughtExceptionHandler();
if (uncaughtExceptionHandler == null) {
uncaughtExceptionHandler = Thread.getDefaultUncaughtExceptionHandler();
}
uncaughtExceptionHandler.uncaughtException(Thread.currentThread(), e);
});
}
private void initializeApplicationMigrations() {
ApplicationMigrations.onApplicationCreate(this, ApplicationDependencies.getJobManager());
}
@@ -335,7 +280,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
private void initializeFirstEverAppLaunch() {
if (TextSecurePreferences.getFirstInstallVersion(this) == -1) {
if (!SignalDatabase.databaseFileExists(this) || VersionTracker.getDaysSinceFirstInstalled(this) < 365) {
if (!SQLCipherOpenHelper.databaseFileExists(this) || VersionTracker.getDaysSinceFirstInstalled(this) < 365) {
Log.i(TAG, "First ever app launch!");
AppInitialization.onFirstEverAppLaunch(this);
}
@@ -351,16 +296,22 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
}
}
private void initializeFcmCheck() {
if (SignalStore.account().isRegistered()) {
long nextSetTime = SignalStore.account().getFcmTokenLastSetTime() + TimeUnit.HOURS.toMillis(6);
private void initializeGcmCheck() {
if (TextSecurePreferences.isPushRegistered(this)) {
long nextSetTime = TextSecurePreferences.getFcmTokenLastSetTime(this) + TimeUnit.HOURS.toMillis(6);
if (SignalStore.account().getFcmToken() == null || nextSetTime <= System.currentTimeMillis()) {
if (TextSecurePreferences.getFcmToken(this) == null || nextSetTime <= System.currentTimeMillis()) {
ApplicationDependencies.getJobManager().add(new FcmRefreshJob());
}
}
}
private void initializeSignedPreKeyCheck() {
if (!TextSecurePreferences.isSignedPreKeyRegistered(this)) {
ApplicationDependencies.getJobManager().add(new CreateSignedPreKeyJob(this));
}
}
private void initializeExpiringMessageManager() {
ApplicationDependencies.getExpiringMessageManager().checkSchedule();
}
@@ -387,6 +338,14 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
private void initializeRingRtc() {
try {
if (RtcDeviceLists.hardwareAECBlocked()) {
WebRtcAudioUtils.setWebRtcBasedAcousticEchoCanceler(true);
}
if (!RtcDeviceLists.openSLESAllowed()) {
WebRtcAudioManager.setBlacklistDeviceForOpenSLESUsage(true);
}
CallManager.initialize(this, new RingRtcLogger());
} catch (UnsatisfiedLinkError e) {
throw new AssertionError("Unable to load ringrtc library", e);
@@ -395,7 +354,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
@WorkerThread
private void initializeCircumvention() {
if (ApplicationDependencies.getSignalServiceNetworkAccess().isCensored()) {
if (new SignalServiceNetworkAccess(ApplicationContext.this).isCensored(ApplicationContext.this)) {
try {
ProviderInstaller.installIfNeeded(ApplicationContext.this);
} catch (Throwable t) {
@@ -404,13 +363,6 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
}
}
private void ensureProfileUploaded() {
if (SignalStore.account().isRegistered() && !SignalStore.registrationValues().hasUploadedProfile() && !Recipient.self().getProfileName().isEmpty()) {
Log.w(TAG, "User has a profile, but has not uploaded one. Uploading now.");
ApplicationDependencies.getJobManager().add(new ProfileUploadJob());
}
}
private void executePendingContactSync() {
if (TextSecurePreferences.needsFullContactSync(this)) {
ApplicationDependencies.getJobManager().add(new MultiDeviceContactUpdateJob(true));
@@ -441,7 +393,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
@WorkerThread
private void initializeCleanup() {
int deleted = SignalDatabase.attachments().deleteAbandonedPreuploadedAttachments();
int deleted = DatabaseFactory.getAttachmentDatabase(this).deleteAbandonedPreuploadedAttachments();
Log.i(TAG, "Deleted " + deleted + " abandoned attachments.");
}

View File

@@ -14,7 +14,6 @@ import android.widget.ImageView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatDelegate;
import androidx.appcompat.widget.Toolbar;
import androidx.core.app.ActivityOptionsCompat;
import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory;
@@ -59,12 +58,6 @@ public final class AvatarPreviewActivity extends PassphraseRequiredActivity {
return ActivityOptionsCompat.makeSceneTransitionAnimation(activity, from, "avatar").toBundle();
}
@Override
protected void attachBaseContext(@NonNull Context newBase) {
getDelegate().setLocalNightMode(AppCompatDelegate.MODE_NIGHT_YES);
super.attachBaseContext(newBase);
}
@Override
protected void onCreate(Bundle savedInstanceState, boolean ready) {
super.onCreate(savedInstanceState, ready);
@@ -139,7 +132,7 @@ public final class AvatarPreviewActivity extends PassphraseRequiredActivity {
findViewById(android.R.id.content).setOnClickListener(v -> fullscreenHelper.toggleUiVisibility());
fullscreenHelper.configureToolbarLayout(findViewById(R.id.toolbar_cutout_spacer), toolbar);
fullscreenHelper.configureToolbarSpacer(findViewById(R.id.toolbar_cutout_spacer));
fullscreenHelper.showAndHideWithSystemUI(getWindow(), findViewById(R.id.toolbar_layout));
}

View File

@@ -26,10 +26,10 @@ import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.stickers.StickerLocator;
import org.whispersystems.libsignal.util.guava.Optional;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.Set;
public interface BindableConversationItem extends Unbindable, GiphyMp4Playable, Colorizable, Multiselectable {
@@ -46,8 +46,7 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable,
boolean hasWallpaper,
boolean isMessageRequestAccepted,
boolean canPlayInline,
@NonNull Colorizer colorizer,
boolean isCondensedMode);
@NonNull Colorizer colorizer);
@NonNull ConversationMessage getConversationMessage();
@@ -57,18 +56,9 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable,
// Intentionally Blank.
}
default void updateContactNameColor() {
// Intentionally Blank.
}
default void updateSelectedState() {
// Intentionally Blank.
}
interface EventListener {
void onQuoteClicked(MmsMessageRecord messageRecord);
void onLinkPreviewClicked(@NonNull LinkPreview linkPreview);
void onQuotedIndicatorClicked(@NonNull MessageRecord messageRecord);
void onMoreTextClicked(@NonNull RecipientId conversationRecipientId, long messageId, boolean isMms);
void onStickerClicked(@NonNull StickerLocator stickerLocator);
void onViewOnceMessageClicked(@NonNull MmsMessageRecord messageRecord);
@@ -76,7 +66,7 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable,
void onAddToContactsClicked(@NonNull Contact contact);
void onMessageSharedContactClicked(@NonNull List<Recipient> choices);
void onInviteSharedContactClicked(@NonNull List<Recipient> choices);
void onReactionClicked(@NonNull MultiselectPart multiselectPart, long messageId, boolean isMms);
void onReactionClicked(@NonNull View reactionTarget, long messageId, boolean isMms);
void onGroupMemberClicked(@NonNull RecipientId recipientId, @NonNull GroupId groupId);
void onMessageWithErrorClicked(@NonNull MessageRecord messageRecord);
void onMessageWithRecaptchaNeededClicked(@NonNull MessageRecord messageRecord);
@@ -98,15 +88,8 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable,
void onInMemoryMessageClicked(@NonNull InMemoryMessageRecord messageRecord);
void onViewGroupDescriptionChange(@Nullable GroupId groupId, @NonNull String description, boolean isMessageRequestAccepted);
void onChangeNumberUpdateContact(@NonNull Recipient recipient);
void onCallToAction(@NonNull String action);
void onDonateClicked();
void onBlockJoinRequest(@NonNull Recipient recipient);
void onRecipientNameClicked(@NonNull RecipientId target);
/** @return true if handled, false if you want to let the normal url handling continue */
boolean onUrlClicked(@NonNull String url);
void onViewGiftBadgeClicked(@NonNull MessageRecord messageRecord);
void onGiftBadgeRevealed(@NonNull MessageRecord messageRecord);
}
}

View File

@@ -1,9 +1,7 @@
package org.thoughtcrime.securesms;
import androidx.annotation.NonNull;
import androidx.lifecycle.LifecycleOwner;
import org.thoughtcrime.securesms.conversationlist.model.ConversationSet;
import org.thoughtcrime.securesms.database.model.ThreadRecord;
import org.thoughtcrime.securesms.mms.GlideRequests;
@@ -12,12 +10,11 @@ import java.util.Set;
public interface BindableConversationListItem extends Unbindable {
void bind(@NonNull LifecycleOwner lifecycleOwner,
@NonNull ThreadRecord thread,
void bind(@NonNull ThreadRecord thread,
@NonNull GlideRequests glideRequests, @NonNull Locale locale,
@NonNull Set<Long> typingThreads,
@NonNull ConversationSet selectedConversations);
@NonNull Set<Long> selectedThreads, boolean batchMode);
void setSelectedConversations(@NonNull ConversationSet conversations);
void setBatchMode(boolean batchMode);
void updateTypingIndicator(@NonNull Set<Long> typingThreads);
}

View File

@@ -11,9 +11,9 @@ import androidx.lifecycle.Lifecycle;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.signal.core.util.concurrent.SimpleTask;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
/**
* This should be used whenever we want to prompt the user to block/unblock a recipient.
@@ -65,7 +65,7 @@ public final class BlockUnblockDialog {
Resources resources = context.getResources();
if (recipient.isGroup()) {
if (SignalDatabase.groups().isActive(recipient.requireGroupId())) {
if (DatabaseFactory.getGroupDatabase(context).isActive(recipient.requireGroupId())) {
builder.setTitle(resources.getString(R.string.BlockUnblockDialog_block_and_leave_s, recipient.getDisplayName(context)));
builder.setMessage(R.string.BlockUnblockDialog_you_will_no_longer_receive_messages_or_updates);
builder.setPositiveButton(R.string.BlockUnblockDialog_block_and_leave, ((dialog, which) -> onBlock.run()));
@@ -76,15 +76,9 @@ public final class BlockUnblockDialog {
builder.setPositiveButton(R.string.RecipientPreferenceActivity_block, ((dialog, which) -> onBlock.run()));
builder.setNegativeButton(android.R.string.cancel, null);
}
} else if (recipient.isReleaseNotes()) {
builder.setTitle(resources.getString(R.string.BlockUnblockDialog_block_s, recipient.getDisplayName(context)));
builder.setMessage(R.string.BlockUnblockDialog_block_getting_signal_updates_and_news);
builder.setPositiveButton(R.string.BlockUnblockDialog_block, ((dialog, which) -> onBlock.run()));
builder.setNegativeButton(android.R.string.cancel, null);
} else {
builder.setTitle(resources.getString(R.string.BlockUnblockDialog_block_s, recipient.getDisplayName(context)));
builder.setMessage(recipient.isRegistered() ? R.string.BlockUnblockDialog_blocked_people_wont_be_able_to_call_you_or_send_you_messages
: R.string.BlockUnblockDialog_blocked_people_wont_be_able_to_send_you_messages);
builder.setMessage(R.string.BlockUnblockDialog_blocked_people_wont_be_able_to_call_you_or_send_you_messages);
if (onBlockAndReportSpam != null) {
builder.setNeutralButton(android.R.string.cancel, null);
@@ -110,7 +104,7 @@ public final class BlockUnblockDialog {
Resources resources = context.getResources();
if (recipient.isGroup()) {
if (SignalDatabase.groups().isActive(recipient.requireGroupId())) {
if (DatabaseFactory.getGroupDatabase(context).isActive(recipient.requireGroupId())) {
builder.setTitle(resources.getString(R.string.BlockUnblockDialog_unblock_s, recipient.getDisplayName(context)));
builder.setMessage(R.string.BlockUnblockDialog_group_members_will_be_able_to_add_you);
builder.setPositiveButton(R.string.RecipientPreferenceActivity_unblock, ((dialog, which) -> onUnblock.run()));
@@ -121,16 +115,9 @@ public final class BlockUnblockDialog {
builder.setPositiveButton(R.string.RecipientPreferenceActivity_unblock, ((dialog, which) -> onUnblock.run()));
builder.setNegativeButton(android.R.string.cancel, null);
}
} else if (recipient.isReleaseNotes()) {
builder.setTitle(resources.getString(R.string.BlockUnblockDialog_unblock_s, recipient.getDisplayName(context)));
builder.setMessage(R.string.BlockUnblockDialog_resume_getting_signal_updates_and_news);
builder.setPositiveButton(R.string.RecipientPreferenceActivity_unblock, ((dialog, which) -> onUnblock.run()));
builder.setNegativeButton(android.R.string.cancel, null);
} else {
builder.setTitle(resources.getString(R.string.BlockUnblockDialog_unblock_s, recipient.getDisplayName(context)));
builder.setMessage(recipient.isRegistered() ? R.string.BlockUnblockDialog_you_will_be_able_to_call_and_message_each_other
: R.string.BlockUnblockDialog_you_will_be_able_to_message_each_other);
builder.setMessage(R.string.BlockUnblockDialog_you_will_be_able_to_call_and_message_each_other);
builder.setPositiveButton(R.string.RecipientPreferenceActivity_unblock, ((dialog, which) -> onUnblock.run()));
builder.setNegativeButton(android.R.string.cancel, null);
}

View File

@@ -20,23 +20,22 @@ import android.content.Context;
import android.os.AsyncTask;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.appcompat.widget.Toolbar;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.components.ContactFilterView;
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader.DisplayMode;
import org.thoughtcrime.securesms.contacts.sync.ContactDiscovery;
import org.thoughtcrime.securesms.contacts.sync.DirectoryHelper;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.ServiceUtil;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.util.guava.Optional;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.util.Optional;
import java.util.function.Consumer;
/**
@@ -124,12 +123,12 @@ public abstract class ContactSelectionActivity extends PassphraseRequiredActivit
}
@Override
public void onBeforeContactSelected(@NonNull Optional<RecipientId> recipientId, String number, @NonNull Consumer<Boolean> callback) {
public void onBeforeContactSelected(Optional<RecipientId> recipientId, String number, Consumer<Boolean> callback) {
callback.accept(true);
}
@Override
public void onContactDeselected(@NonNull Optional<RecipientId> recipientId, String number) {}
public void onContactDeselected(Optional<RecipientId> recipientId, String number) {}
@Override
public void onBeginScroll() {
@@ -153,7 +152,7 @@ public abstract class ContactSelectionActivity extends PassphraseRequiredActivit
@Override
protected Void doInBackground(Context... params) {
try {
ContactDiscovery.refreshAll(params[0], true);
DirectoryHelper.refreshDirectory(params[0], true);
} catch (IOException e) {
Log.w(TAG, e);
}

View File

@@ -18,19 +18,21 @@ package org.thoughtcrime.securesms;
import android.Manifest;
import android.animation.LayoutTransition;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.graphics.Rect;
import android.os.AsyncTask;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.HorizontalScrollView;
import android.widget.TextView;
import android.widget.Toast;
@@ -41,7 +43,6 @@ import androidx.appcompat.app.AlertDialog;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.constraintlayout.widget.ConstraintSet;
import androidx.fragment.app.FragmentActivity;
import androidx.lifecycle.ViewModelProvider;
import androidx.loader.app.LoaderManager;
import androidx.loader.content.Loader;
import androidx.recyclerview.widget.DefaultItemAnimator;
@@ -53,64 +54,58 @@ import androidx.transition.TransitionManager;
import com.annimon.stream.Collectors;
import com.annimon.stream.Stream;
import com.google.android.material.chip.ChipGroup;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.pnikosis.materialishprogress.ProgressWheel;
import org.signal.core.util.concurrent.SimpleTask;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.components.RecyclerViewFastScroller;
import org.thoughtcrime.securesms.components.recyclerview.ToolbarShadowAnimationHelper;
import org.thoughtcrime.securesms.contacts.AbstractContactsCursorLoader;
import org.thoughtcrime.securesms.contacts.ContactChipViewModel;
import org.thoughtcrime.securesms.contacts.ContactChip;
import org.thoughtcrime.securesms.contacts.ContactSelectionListAdapter;
import org.thoughtcrime.securesms.contacts.ContactSelectionListItem;
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader;
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader.DisplayMode;
import org.thoughtcrime.securesms.contacts.HeaderAction;
import org.thoughtcrime.securesms.contacts.LetterHeaderDecoration;
import org.thoughtcrime.securesms.contacts.SelectedContact;
import org.thoughtcrime.securesms.contacts.SelectedContacts;
import org.thoughtcrime.securesms.contacts.selection.ContactSelectionArguments;
import org.thoughtcrime.securesms.contacts.sync.ContactDiscovery;
import org.thoughtcrime.securesms.contacts.sync.DirectoryHelper;
import org.thoughtcrime.securesms.groups.SelectionLimits;
import org.thoughtcrime.securesms.groups.ui.GroupLimitDialog;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.recipients.LiveRecipient;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.sharing.ShareContact;
import org.thoughtcrime.securesms.util.LifecycleDisposable;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.UsernameUtil;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.adapter.FixedViewsAdapter;
import org.thoughtcrime.securesms.util.adapter.RecyclerViewConcatenateAdapterStickyHeader;
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter;
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModelList;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
import org.whispersystems.libsignal.util.guava.Optional;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.function.Consumer;
import io.reactivex.rxjava3.disposables.Disposable;
import kotlin.Unit;
/**
* Fragment for selecting a one or more contacts from a list.
*
* @author Moxie Marlinspike
*
*/
public final class ContactSelectionListFragment extends LoggingFragment
implements LoaderManager.LoaderCallbacks<Cursor>
implements LoaderManager.LoaderCallbacks<Cursor>
{
@SuppressWarnings("unused")
private static final String TAG = Log.tag(ContactSelectionListFragment.class);
private static final int CHIP_GROUP_EMPTY_CHILD_COUNT = 0;
private static final int CHIP_GROUP_EMPTY_CHILD_COUNT = 1;
private static final int CHIP_GROUP_REVEAL_DURATION_MS = 150;
public static final int NO_LIMIT = Integer.MAX_VALUE;
@@ -138,26 +133,24 @@ public final class ContactSelectionListFragment extends LoggingFragment
private RecyclerView recyclerView;
private RecyclerViewFastScroller fastScroller;
private ContactSelectionListAdapter cursorRecyclerViewAdapter;
private RecyclerView chipRecycler;
private ChipGroup chipGroup;
private HorizontalScrollView chipGroupScrollContainer;
private OnSelectionLimitReachedListener onSelectionLimitReachedListener;
private AbstractContactsCursorLoaderFactoryProvider cursorFactoryProvider;
private MappingAdapter contactChipAdapter;
private ContactChipViewModel contactChipViewModel;
private LifecycleDisposable lifecycleDisposable;
private View shadowView;
private ToolbarShadowAnimationHelper toolbarShadowAnimationHelper;
private HeaderActionProvider headerActionProvider;
private TextView headerActionView;
@Nullable private FixedViewsAdapter headerAdapter;
@Nullable private FixedViewsAdapter footerAdapter;
@Nullable private ListCallback listCallback;
@Nullable private ScrollCallback scrollCallback;
private GlideRequests glideRequests;
private SelectionLimits selectionLimit = SelectionLimits.NO_LIMITS;
private Set<RecipientId> currentSelection;
private boolean isMulti;
private boolean hideCount;
private boolean canSelectSelf;
private GlideRequests glideRequests;
private SelectionLimits selectionLimit = SelectionLimits.NO_LIMITS;
private Set<RecipientId> currentSelection;
private boolean isMulti;
private boolean hideCount;
private boolean canSelectSelf;
@Override
public void onAttach(@NonNull Context context) {
@@ -198,14 +191,6 @@ public final class ContactSelectionListFragment extends LoggingFragment
if (getParentFragment() instanceof AbstractContactsCursorLoaderFactoryProvider) {
cursorFactoryProvider = (AbstractContactsCursorLoaderFactoryProvider) getParentFragment();
}
if (context instanceof HeaderActionProvider) {
headerActionProvider = (HeaderActionProvider) context;
}
if (getParentFragment() instanceof HeaderActionProvider) {
headerActionProvider = (HeaderActionProvider) getParentFragment();
}
}
@Override
@@ -255,13 +240,15 @@ public final class ContactSelectionListFragment extends LoggingFragment
showContactsButton = view.findViewById(R.id.show_contacts_button);
showContactsDescription = view.findViewById(R.id.show_contacts_description);
showContactsProgress = view.findViewById(R.id.progress);
chipRecycler = view.findViewById(R.id.chipRecycler);
chipGroup = view.findViewById(R.id.chipGroup);
chipGroupScrollContainer = view.findViewById(R.id.chipGroupScrollContainer);
constraintLayout = view.findViewById(R.id.container);
headerActionView = view.findViewById(R.id.header_action);
shadowView = view.findViewById(R.id.toolbar_shadow);
final LinearLayoutManager layoutManager = new LinearLayoutManager(requireContext());
toolbarShadowAnimationHelper = new ToolbarShadowAnimationHelper(shadowView);
recyclerView.setLayoutManager(layoutManager);
recyclerView.addOnScrollListener(toolbarShadowAnimationHelper);
recyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
recyclerView.setItemAnimator(new DefaultItemAnimator() {
@Override
public boolean canReuseUpdatedViewHolder(@NonNull RecyclerView.ViewHolder viewHolder) {
@@ -269,18 +256,6 @@ public final class ContactSelectionListFragment extends LoggingFragment
}
});
contactChipViewModel = new ViewModelProvider(this).get(ContactChipViewModel.class);
contactChipAdapter = new MappingAdapter();
lifecycleDisposable = new LifecycleDisposable();
lifecycleDisposable.bindTo(getViewLifecycleOwner());
SelectedContacts.register(contactChipAdapter, this::onChipCloseIconClicked);
chipRecycler.setAdapter(contactChipAdapter);
Disposable disposable = contactChipViewModel.getState().subscribe(this::handleSelectedContactsChanged);
lifecycleDisposable.add(disposable);
Intent intent = requireActivity().getIntent();
Bundle arguments = safeArguments();
@@ -311,40 +286,6 @@ public final class ContactSelectionListFragment extends LoggingFragment
currentSelection = getCurrentSelection();
final HeaderAction headerAction;
if (headerActionProvider != null) {
headerAction = headerActionProvider.getHeaderAction();
headerActionView.setEnabled(true);
headerActionView.setText(headerAction.getLabel());
headerActionView.setCompoundDrawablesRelativeWithIntrinsicBounds(headerAction.getIcon(), 0, 0, 0);
headerActionView.setOnClickListener(v -> headerAction.getAction().run());
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
private final Rect bounds = new Rect();
@Override
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
}
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
if (hideLetterHeaders()) {
return;
}
int firstPosition = layoutManager.findFirstVisibleItemPosition();
if (firstPosition == 0) {
View firstChild = recyclerView.getChildAt(0);
recyclerView.getDecoratedBoundsWithMargins(firstChild, bounds);
headerActionView.setTranslationY(bounds.top);
}
}
});
} else {
headerActionView.setEnabled(false);
}
return view;
}
@@ -403,8 +344,7 @@ public final class ContactSelectionListFragment extends LoggingFragment
null,
new ListClickListener(),
isMulti,
currentSelection,
safeArguments().getInt(ContactSelectionArguments.CHECKBOX_RESOURCE, R.drawable.contact_selection_checkbox));
currentSelection);
RecyclerViewConcatenateAdapterStickyHeader concatenateAdapter = new RecyclerViewConcatenateAdapterStickyHeader();
@@ -552,19 +492,12 @@ public final class ContactSelectionListFragment extends LoggingFragment
fastScroller.setRecyclerView(null);
fastScroller.setVisibility(View.GONE);
}
if (headerActionView.isEnabled() && !hasQueryFilter()) {
headerActionView.setVisibility(View.VISIBLE);
} else {
headerActionView.setVisibility(View.GONE);
}
}
@Override
public void onLoaderReset(@NonNull Loader<Cursor> loader) {
cursorRecyclerViewAdapter.changeCursor(null);
fastScroller.setVisibility(View.GONE);
headerActionView.setVisibility(View.GONE);
}
private boolean shouldDisplayRecents() {
@@ -589,7 +522,7 @@ public final class ContactSelectionListFragment extends LoggingFragment
@Override
protected Boolean doInBackground(Void... voids) {
try {
ContactDiscovery.refreshAll(context, false);
DirectoryHelper.refreshDirectory(context, false);
return true;
} catch (IOException e) {
Log.w(TAG, e);
@@ -614,44 +547,11 @@ public final class ContactSelectionListFragment extends LoggingFragment
}.execute();
}
/**
* Allows the caller to submit a list of recipients to be marked selected. Useful for when a screen needs to load preselected
* entries in the background before setting them in the adapter.
*
* @param contacts List of the contacts to select. This will not overwrite the current selection, but append to it.
*/
public void markSelected(@NonNull Set<ShareContact> contacts) {
if (contacts.isEmpty()) {
return;
}
Set<SelectedContact> toMarkSelected = contacts.stream()
.map(contact -> {
if (contact.getRecipientId().isPresent()) {
return SelectedContact.forRecipientId(contact.getRecipientId().get());
} else {
return SelectedContact.forPhone(null, contact.getNumber());
}
})
.filter(c -> !cursorRecyclerViewAdapter.isSelectedContact(c))
.collect(java.util.stream.Collectors.toSet());
if (toMarkSelected.isEmpty()) {
return;
}
for (final SelectedContact selectedContact : toMarkSelected) {
markContactSelected(selectedContact);
}
cursorRecyclerViewAdapter.notifyItemRangeChanged(0, cursorRecyclerViewAdapter.getItemCount());
}
private class ListClickListener implements ContactSelectionListAdapter.ItemClickListener {
@Override
public void onItemClick(ContactSelectionListItem contact) {
SelectedContact selectedContact = contact.isUsernameType() ? SelectedContact.forUsername(contact.getRecipientId().orElse(null), contact.getNumber())
: SelectedContact.forPhone(contact.getRecipientId().orElse(null), contact.getNumber());
SelectedContact selectedContact = contact.isUsernameType() ? SelectedContact.forUsername(contact.getRecipientId().orNull(), contact.getNumber())
: SelectedContact.forPhone(contact.getRecipientId().orNull(), contact.getNumber());
if (!canSelectSelf && Recipient.self().getId().equals(selectedContact.getOrCreateRecipientId(requireContext()))) {
Toast.makeText(requireContext(), R.string.ContactSelectionListFragment_you_do_not_need_to_add_yourself_to_the_group, Toast.LENGTH_SHORT).show();
@@ -672,12 +572,12 @@ public final class ContactSelectionListFragment extends LoggingFragment
AlertDialog loadingDialog = SimpleProgressDialog.show(requireContext());
SimpleTask.run(getViewLifecycleOwner().getLifecycle(), () -> {
return UsernameUtil.fetchAciForUsername(contact.getNumber());
return UsernameUtil.fetchUuidForUsername(requireContext(), contact.getNumber());
}, uuid -> {
loadingDialog.dismiss();
if (uuid.isPresent()) {
Recipient recipient = Recipient.externalUsername(uuid.get(), contact.getNumber());
SelectedContact selected = SelectedContact.forUsername(recipient.getId(), contact.getNumber());
Recipient recipient = Recipient.externalUsername(requireContext(), uuid.get(), contact.getNumber());
SelectedContact selected = SelectedContact.forUsername(recipient.getId(), contact.getNumber());
if (onContactSelectedListener != null) {
onContactSelectedListener.onBeforeContactSelected(Optional.of(recipient.getId()), null, allowed -> {
@@ -747,22 +647,75 @@ public final class ContactSelectionListFragment extends LoggingFragment
private void markContactUnselected(@NonNull SelectedContact selectedContact) {
cursorRecyclerViewAdapter.removeFromSelectedContacts(selectedContact);
cursorRecyclerViewAdapter.notifyItemRangeChanged(0, cursorRecyclerViewAdapter.getItemCount(), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
contactChipViewModel.remove(selectedContact);
removeChipForContact(selectedContact);
if (onContactSelectedListener != null) {
onContactSelectedListener.onSelectionChanged();
}
}
private void handleSelectedContactsChanged(@NonNull List<SelectedContacts.Model> selectedContacts) {
contactChipAdapter.submitList(new MappingModelList(selectedContacts), this::smoothScrollChipsToEnd);
private void removeChipForContact(@NonNull SelectedContact contact) {
for (int i = chipGroup.getChildCount() - 1; i >= 0; i--) {
View v = chipGroup.getChildAt(i);
if (v instanceof ContactChip && contact.matches(((ContactChip) v).getContact())) {
chipGroup.removeView(v);
}
}
if (selectedContacts.isEmpty()) {
if (getChipCount() == 0) {
setChipGroupVisibility(ConstraintSet.GONE);
} else {
}
}
private void addChipForSelectedContact(@NonNull SelectedContact selectedContact) {
SimpleTask.run(getViewLifecycleOwner().getLifecycle(),
() -> Recipient.resolved(selectedContact.getOrCreateRecipientId(requireContext())),
resolved -> addChipForRecipient(resolved, selectedContact));
}
private void addChipForRecipient(@NonNull Recipient recipient, @NonNull SelectedContact selectedContact) {
final ContactChip chip = new ContactChip(requireContext());
if (getChipCount() == 0) {
setChipGroupVisibility(ConstraintSet.VISIBLE);
}
chip.setText(recipient.getShortDisplayName(requireContext()));
chip.setContact(selectedContact);
chip.setCloseIconVisible(true);
chip.setOnCloseIconClickListener(view -> {
markContactUnselected(selectedContact);
if (onContactSelectedListener != null) {
onContactSelectedListener.onContactDeselected(Optional.of(recipient.getId()), recipient.getE164().orNull());
}
});
chipGroup.getLayoutTransition().addTransitionListener(new LayoutTransition.TransitionListener() {
@Override
public void startTransition(LayoutTransition transition, ViewGroup container, View view, int transitionType) {
}
@Override
public void endTransition(LayoutTransition transition, ViewGroup container, View view, int transitionType) {
if (getView() == null || !requireView().isAttachedToWindow()) {
Log.w(TAG, "Fragment's view was detached before the animation completed.");
return;
}
if (view == chip && transitionType == LayoutTransition.APPEARING) {
chipGroup.getLayoutTransition().removeTransitionListener(this);
registerChipRecipientObserver(chip, recipient.live());
chipGroup.post(ContactSelectionListFragment.this::smoothScrollChipsToEnd);
}
}
});
chip.setAvatar(glideRequests, recipient, () -> addChip(chip));
}
private void addChip(@NonNull ContactChip chip) {
chipGroup.addView(chip);
if (selectionWarningLimitReachedExactly()) {
if (onSelectionLimitReachedListener != null) {
onSelectionLimitReachedListener.onSuggestedLimitReached(selectionLimit.getRecommendedLimit());
@@ -772,41 +725,33 @@ public final class ContactSelectionListFragment extends LoggingFragment
}
}
private void addChipForSelectedContact(@NonNull SelectedContact selectedContact) {
SimpleTask.run(getViewLifecycleOwner().getLifecycle(),
() -> Recipient.resolved(selectedContact.getOrCreateRecipientId(requireContext())),
resolved -> contactChipViewModel.add(selectedContact));
}
private Unit onChipCloseIconClicked(SelectedContacts.Model model) {
markContactUnselected(model.getSelectedContact());
if (onContactSelectedListener != null) {
onContactSelectedListener.onContactDeselected(Optional.of(model.getRecipient().getId()), model.getRecipient().getE164().orElse(null));
}
return Unit.INSTANCE;
}
private int getChipCount() {
int count = contactChipViewModel.getCount() - CHIP_GROUP_EMPTY_CHILD_COUNT;
int count = chipGroup.getChildCount() - CHIP_GROUP_EMPTY_CHILD_COUNT;
if (count < 0) throw new AssertionError();
return count;
}
private void registerChipRecipientObserver(@NonNull ContactChip chip, @Nullable LiveRecipient recipient) {
if (recipient != null) {
recipient.observe(getViewLifecycleOwner(), resolved -> {
if (chip.isAttachedToWindow()) {
chip.setAvatar(glideRequests, resolved, null);
chip.setText(resolved.getShortDisplayName(chip.getContext()));
}
});
}
}
private void setChipGroupVisibility(int visibility) {
if (!safeArguments().getBoolean(DISPLAY_CHIPS, requireActivity().getIntent().getBooleanExtra(DISPLAY_CHIPS, true))) {
return;
}
AutoTransition transition = new AutoTransition();
transition.setDuration(CHIP_GROUP_REVEAL_DURATION_MS);
transition.excludeChildren(recyclerView, true);
transition.excludeTarget(recyclerView, true);
TransitionManager.beginDelayedTransition(constraintLayout, transition);
TransitionManager.beginDelayedTransition(constraintLayout, new AutoTransition().setDuration(CHIP_GROUP_REVEAL_DURATION_MS));
ConstraintSet constraintSet = new ConstraintSet();
constraintSet.clone(constraintLayout);
constraintSet.setVisibility(R.id.chipRecycler, visibility);
constraintSet.setVisibility(R.id.chipGroupScrollContainer, visibility);
constraintSet.applyTo(constraintLayout);
}
@@ -815,30 +760,24 @@ public final class ContactSelectionListFragment extends LoggingFragment
}
private void smoothScrollChipsToEnd() {
int x = ViewUtil.isLtr(chipRecycler) ? chipRecycler.getWidth() : 0;
chipRecycler.smoothScrollBy(x, 0);
int x = ViewUtil.isLtr(chipGroupScrollContainer) ? chipGroup.getWidth() : 0;
chipGroupScrollContainer.smoothScrollTo(x, 0);
}
public interface OnContactSelectedListener {
/**
* Provides an opportunity to disallow selecting an item. Call the callback with false to disallow, or true to allow it.
*/
void onBeforeContactSelected(@NonNull Optional<RecipientId> recipientId, @Nullable String number, @NonNull Consumer<Boolean> callback);
void onContactDeselected(@NonNull Optional<RecipientId> recipientId, @Nullable String number);
/** Provides an opportunity to disallow selecting an item. Call the callback with false to disallow, or true to allow it. */
void onBeforeContactSelected(Optional<RecipientId> recipientId, String number, Consumer<Boolean> callback);
void onContactDeselected(Optional<RecipientId> recipientId, String number);
void onSelectionChanged();
}
public interface OnSelectionLimitReachedListener {
void onSuggestedLimitReached(int limit);
void onHardLimitReached(int limit);
}
public interface ListCallback {
void onInvite();
void onNewGroup(boolean forceV1);
}
@@ -846,10 +785,6 @@ public final class ContactSelectionListFragment extends LoggingFragment
void onBeginScroll();
}
public interface HeaderActionProvider {
@NonNull HeaderAction getHeaderAction();
}
public interface AbstractContactsCursorLoaderFactoryProvider {
@NonNull AbstractContactsCursorLoader.Factory get();
}

View File

@@ -17,25 +17,26 @@ import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.widget.Toolbar;
import androidx.core.content.ContextCompat;
import org.signal.core.util.ThreadUtil;
import org.signal.core.util.logging.Log;
import org.signal.libsignal.protocol.IdentityKeyPair;
import org.signal.libsignal.protocol.InvalidKeyException;
import org.signal.libsignal.protocol.ecc.Curve;
import org.signal.libsignal.protocol.ecc.ECPublicKey;
import org.signal.libsignal.zkgroup.profiles.ProfileKey;
import org.signal.qr.kitkat.ScanListener;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.qr.ScanListener;
import org.thoughtcrime.securesms.util.Base64;
import org.thoughtcrime.securesms.util.DynamicLanguage;
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
import org.whispersystems.libsignal.IdentityKeyPair;
import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.libsignal.ecc.Curve;
import org.whispersystems.libsignal.ecc.ECPublicKey;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.push.exceptions.NotFoundException;
import org.whispersystems.signalservice.internal.push.DeviceLimitExceededException;
@@ -119,7 +120,7 @@ public class DeviceActivity extends PassphraseRequiredActivity
}
@Override
public void onQrDataFound(@NonNull final String data) {
public void onQrDataFound(final String data) {
ThreadUtil.runOnMain(() -> {
((Vibrator)getSystemService(Context.VIBRATOR_SERVICE)).vibrate(50);
Uri uri = Uri.parse(data);
@@ -185,13 +186,13 @@ public class DeviceActivity extends PassphraseRequiredActivity
return BAD_CODE;
}
ECPublicKey publicKey = Curve.decodePoint(Base64.decode(publicKeyEncoded), 0);
IdentityKeyPair aciIdentityKeyPair = SignalStore.account().getAciIdentityKey();
IdentityKeyPair pniIdentityKeyPair = SignalStore.account().getPniIdentityKey();
ProfileKey profileKey = ProfileKeyUtil.getSelfProfileKey();
ECPublicKey publicKey = Curve.decodePoint(Base64.decode(publicKeyEncoded), 0);
IdentityKeyPair identityKeyPair = IdentityKeyUtil.getIdentityKeyPair(context);
Optional<byte[]> profileKey = Optional.of(ProfileKeyUtil.getProfileKey(getContext()));
TextSecurePreferences.setMultiDevice(DeviceActivity.this, true);
accountManager.addDevice(ephemeralId, publicKey, aciIdentityKeyPair, pniIdentityKeyPair, profileKey, verificationCode);
TextSecurePreferences.setIsUnidentifiedDeliveryEnabled(context, false);
accountManager.addDevice(ephemeralId, publicKey, identityKeyPair, profileKey, verificationCode);
return SUCCESS;
} catch (NotFoundException e) {

View File

@@ -14,34 +14,36 @@ import android.widget.ImageView;
import android.widget.LinearLayout;
import androidx.annotation.NonNull;
import androidx.core.view.ViewCompat;
import org.signal.qr.QrScannerView;
import org.signal.qr.kitkat.ScanListener;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.LifecycleDisposable;
import org.thoughtcrime.securesms.components.camera.CameraView;
import org.thoughtcrime.securesms.qr.ScanListener;
import org.thoughtcrime.securesms.qr.ScanningThread;
import org.thoughtcrime.securesms.util.ViewUtil;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.disposables.Disposable;
public class DeviceAddFragment extends LoggingFragment {
private final LifecycleDisposable lifecycleDisposable = new LifecycleDisposable();
private ImageView devicesImage;
private ScanListener scanListener;
private ViewGroup container;
private LinearLayout overlay;
private ImageView devicesImage;
private CameraView scannerView;
private ScanningThread scanningThread;
private ScanListener scanListener;
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup viewGroup, Bundle bundle) {
ViewGroup container = ViewUtil.inflate(inflater, viewGroup, R.layout.device_add_fragment);
this.container = ViewUtil.inflate(inflater, viewGroup, R.layout.device_add_fragment);
this.overlay = this.container.findViewById(R.id.overlay);
this.scannerView = this.container.findViewById(R.id.scanner);
this.devicesImage = this.container.findViewById(R.id.devices);
QrScannerView scannerView = container.findViewById(R.id.scanner);
this.devicesImage = container.findViewById(R.id.devices);
ViewCompat.setTransitionName(devicesImage, "devices");
if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
this.overlay.setOrientation(LinearLayout.HORIZONTAL);
} else {
this.overlay.setOrientation(LinearLayout.VERTICAL);
}
if (Build.VERSION.SDK_INT >= 21) {
container.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
this.container.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
@TargetApi(21)
@Override
public void onLayoutChange(View v, int left, int top, int right, int bottom,
@@ -57,30 +59,52 @@ public class DeviceAddFragment extends LoggingFragment {
});
}
scannerView.start(getViewLifecycleOwner(), FeatureFlags.useQrLegacyScan());
lifecycleDisposable.bindTo(getViewLifecycleOwner());
Disposable qrDisposable = scannerView
.getQrData()
.distinctUntilChanged()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(qrData -> {
if (scanListener != null) {
scanListener.onQrDataFound(qrData);
}
});
lifecycleDisposable.add(qrDisposable);
return container;
return this.container;
}
@Override
public void onResume() {
super.onResume();
this.scanningThread = new ScanningThread();
this.scanningThread.setScanListener(scanListener);
this.scannerView.onResume();
this.scannerView.setPreviewCallback(scanningThread);
this.scanningThread.start();
}
@Override
public void onPause() {
super.onPause();
this.scannerView.onPause();
this.scanningThread.stopScanning();
}
@Override
public void onConfigurationChanged(@NonNull Configuration newConfiguration) {
super.onConfigurationChanged(newConfiguration);
this.scannerView.onPause();
if (newConfiguration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
overlay.setOrientation(LinearLayout.HORIZONTAL);
} else {
overlay.setOrientation(LinearLayout.VERTICAL);
}
this.scannerView.onResume();
this.scannerView.setPreviewCallback(scanningThread);
}
public ImageView getDevicesImage() {
return devicesImage;
}
public void setScanListener(ScanListener scanListener) {
this.scanListener = scanListener;
if (this.scanningThread != null) {
this.scanningThread.setScanListener(scanListener);
}
}
}

View File

@@ -9,7 +9,6 @@ import android.view.ViewGroup;
import android.widget.LinearLayout;
import androidx.annotation.NonNull;
import androidx.core.view.ViewCompat;
import androidx.fragment.app.Fragment;
public class DeviceLinkFragment extends Fragment implements View.OnClickListener {
@@ -22,7 +21,6 @@ public class DeviceLinkFragment extends Fragment implements View.OnClickListener
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup viewGroup, Bundle bundle) {
this.container = (LinearLayout) inflater.inflate(R.layout.device_link_fragment, container, false);
this.container.findViewById(R.id.link_device).setOnClickListener(this);
ViewCompat.setTransitionName(container.findViewById(R.id.devices), "devices");
if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
container.setOrientation(LinearLayout.HORIZONTAL);

View File

@@ -3,6 +3,7 @@ package org.thoughtcrime.securesms;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.content.DialogInterface;
import android.os.AsyncTask;
import android.os.Bundle;
import android.view.LayoutInflater;
@@ -21,7 +22,7 @@ import androidx.loader.app.LoaderManager;
import androidx.loader.content.Loader;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.melnykov.fab.FloatingActionButton;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.database.loaders.DeviceListLoader;

View File

@@ -35,7 +35,6 @@ public final class GroupMembersDialog {
.show();
GroupMemberListView memberListView = dialog.findViewById(R.id.list_members);
memberListView.initializeAdapter(fragmentActivity);
LiveGroup liveGroup = new LiveGroup(groupRecipient.requireGroupId());
LiveData<List<GroupMemberEntry.FullMember>> fullMembers = liveGroup.getFullMembers();

View File

@@ -16,7 +16,6 @@ import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.AnimRes;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.Toolbar;
import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
@@ -25,7 +24,7 @@ import org.thoughtcrime.securesms.components.ContactFilterView;
import org.thoughtcrime.securesms.components.ContactFilterView.OnFilterChangedListener;
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader.DisplayMode;
import org.thoughtcrime.securesms.contacts.SelectedContact;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.groups.SelectionLimits;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
@@ -38,8 +37,8 @@ import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture.Listener;
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
import org.thoughtcrime.securesms.util.text.AfterTextChanged;
import org.whispersystems.libsignal.util.guava.Optional;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.function.Consumer;
@@ -135,13 +134,13 @@ public class InviteActivity extends PassphraseRequiredActivity implements Contac
}
@Override
public void onBeforeContactSelected(@NonNull Optional<RecipientId> recipientId, String number, @NonNull Consumer<Boolean> callback) {
public void onBeforeContactSelected(Optional<RecipientId> recipientId, String number, Consumer<Boolean> callback) {
updateSmsButtonText(contactsFragment.getSelectedContacts().size() + 1);
callback.accept(true);
}
@Override
public void onContactDeselected(@NonNull Optional<RecipientId> recipientId, String number) {
public void onContactDeselected(Optional<RecipientId> recipientId, String number) {
updateSmsButtonText(contactsFragment.getSelectedContacts().size());
}
@@ -251,12 +250,12 @@ public class InviteActivity extends PassphraseRequiredActivity implements Contac
for (SelectedContact contact : contacts) {
RecipientId recipientId = contact.getOrCreateRecipientId(context);
Recipient recipient = Recipient.resolved(recipientId);
int subscriptionId = recipient.getDefaultSubscriptionId().orElse(-1);
int subscriptionId = recipient.getDefaultSubscriptionId().or(-1);
MessageSender.send(context, new OutgoingTextMessage(recipient, message, subscriptionId), -1L, true, null, null);
if (recipient.getContactUri() != null) {
SignalDatabase.recipients().setHasSentInvite(recipient.getId());
DatabaseFactory.getRecipientDatabase(context).setHasSentInvite(recipient.getId());
}
}

View File

@@ -5,27 +5,19 @@ import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.lifecycle.ViewModelProvider;
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaController;
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaControllerOwner;
import org.thoughtcrime.securesms.devicetransfer.olddevice.OldDeviceTransferLockedDialog;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.stories.Stories;
import org.thoughtcrime.securesms.stories.tabs.ConversationListTabRepository;
import org.thoughtcrime.securesms.stories.tabs.ConversationListTabsViewModel;
import org.thoughtcrime.securesms.util.AppStartup;
import org.thoughtcrime.securesms.util.CachedInflater;
import org.thoughtcrime.securesms.util.CommunicationActions;
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.SplashScreenUtil;
import org.thoughtcrime.securesms.util.WindowUtil;
public class MainActivity extends PassphraseRequiredActivity implements VoiceNoteMediaControllerOwner {
@@ -34,14 +26,13 @@ public class MainActivity extends PassphraseRequiredActivity implements VoiceNot
private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
private final MainNavigator navigator = new MainNavigator(this);
private VoiceNoteMediaController mediaController;
private ConversationListTabsViewModel conversationListTabsViewModel;
private VoiceNoteMediaController mediaController;
public static @NonNull Intent clearTop(@NonNull Context context) {
Intent intent = new Intent(context, MainActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP |
Intent.FLAG_ACTIVITY_NEW_TASK |
Intent.FLAG_ACTIVITY_NEW_TASK |
Intent.FLAG_ACTIVITY_SINGLE_TOP);
return intent;
@@ -51,28 +42,22 @@ public class MainActivity extends PassphraseRequiredActivity implements VoiceNot
protected void onCreate(Bundle savedInstanceState, boolean ready) {
AppStartup.getInstance().onCriticalRenderEventStart();
super.onCreate(savedInstanceState, ready);
setContentView(R.layout.main_activity);
mediaController = new VoiceNoteMediaController(this);
ConversationListTabRepository repository = new ConversationListTabRepository();
ConversationListTabsViewModel.Factory factory = new ConversationListTabsViewModel.Factory(repository);
navigator.onCreate(savedInstanceState);
handleGroupLinkInIntent(getIntent());
handleProxyInIntent(getIntent());
handleSignalMeIntent(getIntent());
CachedInflater.from(this).clear();
conversationListTabsViewModel = new ViewModelProvider(this, factory).get(ConversationListTabsViewModel.class);
updateTabVisibility();
}
@Override
public Intent getIntent() {
return super.getIntent().setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP |
Intent.FLAG_ACTIVITY_NEW_TASK |
Intent.FLAG_ACTIVITY_NEW_TASK |
Intent.FLAG_ACTIVITY_SINGLE_TOP);
}
@@ -97,14 +82,6 @@ public class MainActivity extends PassphraseRequiredActivity implements VoiceNot
if (SignalStore.misc().isOldDeviceTransferLocked()) {
OldDeviceTransferLockedDialog.show(getSupportFragmentManager());
}
updateTabVisibility();
}
@Override
protected void onStop() {
super.onStop();
SplashScreenUtil.setSplashScreenThemeIfNecessary(this, SignalStore.settings().getTheme());
}
@Override
@@ -122,17 +99,6 @@ public class MainActivity extends PassphraseRequiredActivity implements VoiceNot
}
}
private void updateTabVisibility() {
if (Stories.isFeatureEnabled()) {
findViewById(R.id.conversation_list_tabs).setVisibility(View.VISIBLE);
WindowUtil.setNavigationBarColor(this, ContextCompat.getColor(this, R.color.signal_colorSurface2));
} else {
findViewById(R.id.conversation_list_tabs).setVisibility(View.GONE);
WindowUtil.setNavigationBarColor(this, ContextCompat.getColor(this, R.color.signal_colorBackground));
conversationListTabsViewModel.onChatsSelected();
}
}
public @NonNull MainNavigator getNavigator() {
return navigator;
}

View File

@@ -2,13 +2,18 @@ package org.thoughtcrime.securesms;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import org.thoughtcrime.securesms.components.settings.DSLSettingsActivity;
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity;
import org.thoughtcrime.securesms.conversation.ConversationIntents;
import org.thoughtcrime.securesms.conversationlist.ConversationListArchiveFragment;
import org.thoughtcrime.securesms.conversationlist.ConversationListFragment;
import org.thoughtcrime.securesms.groups.ui.creategroup.CreateGroupActivity;
import org.thoughtcrime.securesms.insights.InsightsLauncher;
import org.thoughtcrime.securesms.recipients.RecipientId;
@@ -31,6 +36,16 @@ public class MainNavigator {
return ((MainActivity) activity).getNavigator();
}
public void onCreate(@Nullable Bundle savedInstanceState) {
if (savedInstanceState != null) {
return;
}
getFragmentManager().beginTransaction()
.add(R.id.fragment_container, ConversationListFragment.newInstance())
.commit();
}
/**
* @return True if the back pressed was handled in our own custom way, false if it should be given
* to the system to do the default behavior.
@@ -59,6 +74,14 @@ public class MainNavigator {
activity.startActivityForResult(AppSettingsActivity.home(activity), REQUEST_CONFIG_CHANGES);
}
public void goToArchiveList() {
getFragmentManager().beginTransaction()
.setCustomAnimations(R.anim.slide_from_end, R.anim.slide_to_start, R.anim.slide_from_start, R.anim.slide_to_end)
.replace(R.id.fragment_container, ConversationListArchiveFragment.newInstance())
.addToBackStack(null)
.commit();
}
public void goToGroupCreation() {
activity.startActivity(CreateGroupActivity.newIntent(activity));
}

View File

@@ -21,11 +21,13 @@ import android.annotation.SuppressLint;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.database.ContentObserver;
import android.database.Cursor;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
@@ -55,10 +57,6 @@ import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.animation.DepthPageTransformer;
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
import org.thoughtcrime.securesms.components.viewpager.ExtendedOnPageChangedListener;
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaController;
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaControllerOwner;
import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardFragment;
import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardFragmentArgs;
import org.thoughtcrime.securesms.database.MediaDatabase;
import org.thoughtcrime.securesms.database.MediaDatabase.MediaRecord;
import org.thoughtcrime.securesms.database.loaders.PagingMediaLoader;
@@ -71,10 +69,10 @@ import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.sharing.ShareActivity;
import org.thoughtcrime.securesms.util.AttachmentUtil;
import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.FullscreenHelper;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
import org.thoughtcrime.securesms.util.SaveAttachmentTask.Attachment;
import org.thoughtcrime.securesms.util.StorageUtil;
@@ -90,8 +88,7 @@ import java.util.Objects;
public final class MediaPreviewActivity extends PassphraseRequiredActivity
implements LoaderManager.LoaderCallbacks<Pair<Cursor, Integer>>,
MediaRailAdapter.RailItemListener,
MediaPreviewFragment.Events,
VoiceNoteMediaControllerOwner
MediaPreviewFragment.Events
{
private final static String TAG = Log.tag(MediaPreviewActivity.class);
@@ -131,8 +128,6 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
private MediaDatabase.Sorting sorting;
private FullscreenHelper fullscreenHelper;
private VoiceNoteMediaController voiceNoteMediaController;
private @Nullable Cursor cursor = null;
public static @NonNull Intent intentFromMediaRecord(@NonNull Context context,
@@ -165,7 +160,6 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
setSupportActionBar(findViewById(R.id.toolbar));
voiceNoteMediaController = new VoiceNoteMediaController(this);
viewModel = ViewModelProviders.of(this).get(MediaPreviewViewModel.class);
fullscreenHelper = new FullscreenHelper(this);
@@ -210,25 +204,23 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
else from = "";
if (showThread) {
String titleText = null;
String to = null;
Recipient threadRecipient = mediaItem.threadRecipient;
if (threadRecipient != null) {
if (mediaItem.outgoing) {
if (mediaItem.outgoing || threadRecipient.isGroup()) {
if (threadRecipient.isSelf()) {
titleText = getString(R.string.note_to_self);
from = getString(R.string.note_to_self);
} else {
titleText = getString(R.string.MediaPreviewActivity_you_to_s, threadRecipient.getDisplayName(this));
to = threadRecipient.getDisplayName(this);
}
} else {
if (threadRecipient.isGroup()) {
titleText = getString(R.string.MediaPreviewActivity_s_to_s, from, threadRecipient.getDisplayName(this));
} else {
titleText = getString(R.string.MediaPreviewActivity_s_to_you, from);
}
to = getString(R.string.MediaPreviewActivity_you);
}
}
return titleText != null ? titleText : from;
return to != null ? getString(R.string.MediaPreviewActivity_s_to_s, from, to)
: from;
} else {
return from;
}
@@ -295,7 +287,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
anchorMarginsToBottomInsets(detailsContainer);
fullscreenHelper.configureToolbarLayout(findViewById(R.id.toolbar_cutout_spacer), findViewById(R.id.toolbar));
fullscreenHelper.configureToolbarSpacer(findViewById(R.id.toolbar_cutout_spacer));
fullscreenHelper.showAndHideWithSystemUI(getWindow(), detailsContainer, toolbarLayout);
}
@@ -395,12 +387,10 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
MediaItem mediaItem = getCurrentMediaItem();
if (mediaItem != null) {
MultiselectForwardFragmentArgs.create(
this,
mediaItem.uri,
mediaItem.type,
args -> MultiselectForwardFragment.showBottomSheet(getSupportFragmentManager(), args)
);
Intent composeIntent = new Intent(this, ShareActivity.class);
composeIntent.putExtra(Intent.EXTRA_STREAM, mediaItem.uri);
composeIntent.setType(mediaItem.type);
startActivity(composeIntent);
}
}
@@ -544,7 +534,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
}
public static boolean isContentTypeSupported(final String contentType) {
return MediaUtil.isImageType(contentType) || MediaUtil.isVideoType(contentType);
return contentType != null && (contentType.startsWith("image/") || contentType.startsWith("video/"));
}
@Override
@@ -564,33 +554,40 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
}
cursor = Objects.requireNonNull(data.first);
viewModel.setCursor(this, cursor, leftIsRecent);
int mediaPosition = Objects.requireNonNull(data.second);
CursorPagerAdapter oldAdapter = (CursorPagerAdapter) mediaPager.getAdapter();
if (oldAdapter == null) {
CursorPagerAdapter adapter = new CursorPagerAdapter(getSupportFragmentManager(), this, cursor, mediaPosition, leftIsRecent);
mediaPager.setAdapter(adapter);
adapter.setActive(true);
} else {
oldAdapter.setCursor(cursor, mediaPosition);
oldAdapter.setActive(true);
CursorPagerAdapter adapter = new CursorPagerAdapter(getSupportFragmentManager(),this, cursor, mediaPosition, leftIsRecent);
mediaPager.setAdapter(adapter);
adapter.setActive(true);
viewModel.setCursor(this, cursor, leftIsRecent);
int item = restartItem >= 0 ? restartItem : mediaPosition;
mediaPager.setCurrentItem(item);
if (item == 0) {
viewPagerListener.onPageSelected(0);
}
if (oldAdapter == null || restartItem >= 0) {
int item = restartItem >= 0 ? restartItem : mediaPosition;
mediaPager.setCurrentItem(item);
if (item == 0) {
viewPagerListener.onPageSelected(0);
cursor.registerContentObserver(new ContentObserver(new Handler(getMainLooper())) {
@Override
public void onChange(boolean selfChange) {
onMediaChange();
}
}
});
} else {
mediaNotAvailable();
}
}
private void onMediaChange() {
MediaItemAdapter adapter = (MediaItemAdapter) mediaPager.getAdapter();
if (adapter != null) {
adapter.checkMedia(mediaPager.getCurrentItem());
}
}
@Override
public void onLoaderReset(@NonNull Loader<Pair<Cursor, Integer>> loader) {
@@ -608,15 +605,6 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
finish();
}
@Override
public void onMediaReady() {
}
@Override
public @NonNull VoiceNoteMediaController getVoiceNoteMediaController() {
return voiceNoteMediaController;
}
private class ViewPagerListener extends ExtendedOnPageChangedListener {
@Override
@@ -627,10 +615,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
if (adapter != null) {
MediaItem item = adapter.getMediaItemFor(position);
if (item != null && item.recipient != null) {
item.recipient.live().observe(MediaPreviewActivity.this, r -> initializeActionBar());
}
if (item.recipient != null) item.recipient.live().observe(MediaPreviewActivity.this, r -> initializeActionBar());
viewModel.setActiveAlbumRailItem(MediaPreviewActivity.this, position);
initializeActionBar();
}
@@ -643,9 +628,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
if (adapter != null) {
MediaItem item = adapter.getMediaItemFor(position);
if (item != null && item.recipient != null) {
item.recipient.live().removeObservers(MediaPreviewActivity.this);
}
if (item.recipient != null) item.recipient.live().removeObservers(MediaPreviewActivity.this);
adapter.pause(position);
}
@@ -695,7 +678,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
}
@Override
public @Nullable MediaItem getMediaItemFor(int position) {
public MediaItem getMediaItemFor(int position) {
return new MediaItem(null, null, null, uri, mediaType, -1, true);
}
@@ -718,6 +701,11 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
public boolean hasFragmentFor(int position) {
return mediaPreviewFragment != null;
}
@Override
public void checkMedia(int currentItem) {
}
}
private static void anchorMarginsToBottomInsets(@NonNull View viewToAnchor) {
@@ -741,10 +729,10 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
private final Map<Integer, MediaPreviewFragment> mediaFragments = new HashMap<>();
private final Context context;
private final Cursor cursor;
private final boolean leftIsRecent;
private boolean active;
private Cursor cursor;
private int autoPlayPosition;
CursorPagerAdapter(@NonNull FragmentManager fragmentManager,
@@ -765,11 +753,6 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
notifyDataSetChanged();
}
public void setCursor(@NonNull Cursor cursor, int autoPlayPosition) {
this.cursor = cursor;
this.autoPlayPosition = autoPlayPosition;
}
@Override
public int getCount() {
if (!active) return 0;
@@ -806,15 +789,8 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
super.destroyItem(container, position, object);
}
public @Nullable MediaItem getMediaItemFor(int position) {
int cursorPosition = getCursorPosition(position);
if (cursor.isClosed() || cursorPosition < 0) {
Log.w(TAG, "Invalid cursor state! Closed: " + cursor.isClosed() + " Position: " + cursorPosition);
return null;
}
cursor.moveToPosition(cursorPosition);
public MediaItem getMediaItemFor(int position) {
cursor.moveToPosition(getCursorPosition(position));
MediaRecord mediaRecord = MediaRecord.from(context, cursor);
DatabaseAttachment attachment = Objects.requireNonNull(mediaRecord.getAttachment());
@@ -848,6 +824,14 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
return mediaFragments.containsKey(position);
}
@Override
public void checkMedia(int position) {
MediaPreviewFragment fragment = mediaFragments.get(position);
if (fragment != null) {
fragment.checkMediaStillAvailable();
}
}
private int getCursorPosition(int position) {
if (leftIsRecent) return position;
else return cursor.getCount() - 1 - position;
@@ -882,9 +866,10 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
}
interface MediaItemAdapter {
@Nullable MediaItem getMediaItemFor(int position);
MediaItem getMediaItemFor(int position);
void pause(int position);
@Nullable View getPlaybackControls(int position);
boolean hasFragmentFor(int position);
void checkMedia(int currentItem);
}
}

View File

@@ -1,6 +1,7 @@
package org.thoughtcrime.securesms;
import android.content.Context;
import android.content.DialogInterface;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -32,19 +33,22 @@ public class MuteDialog extends AlertDialog {
public static void show(final Context context, final @NonNull MuteSelectionListener listener, @Nullable Runnable cancelListener) {
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(context);
builder.setTitle(R.string.MuteDialog_mute_notifications);
builder.setItems(R.array.mute_durations, (dialog, which) -> {
final long muteUntil;
builder.setItems(R.array.mute_durations, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, final int which) {
final long muteUntil;
switch (which) {
case 0: muteUntil = System.currentTimeMillis() + TimeUnit.HOURS.toMillis(1); break;
case 1: muteUntil = System.currentTimeMillis() + TimeUnit.HOURS.toMillis(8); break;
case 2: muteUntil = System.currentTimeMillis() + TimeUnit.DAYS.toMillis(1); break;
case 3: muteUntil = System.currentTimeMillis() + TimeUnit.DAYS.toMillis(7); break;
case 4: muteUntil = Long.MAX_VALUE; break;
default: muteUntil = System.currentTimeMillis() + TimeUnit.HOURS.toMillis(1); break;
switch (which) {
case 0: muteUntil = System.currentTimeMillis() + TimeUnit.HOURS.toMillis(1); break;
case 1: muteUntil = System.currentTimeMillis() + TimeUnit.HOURS.toMillis(8); break;
case 2: muteUntil = System.currentTimeMillis() + TimeUnit.DAYS.toMillis(1); break;
case 3: muteUntil = System.currentTimeMillis() + TimeUnit.DAYS.toMillis(7); break;
case 4: muteUntil = Long.MAX_VALUE; break;
default: muteUntil = System.currentTimeMillis() + TimeUnit.HOURS.toMillis(1); break;
}
listener.onMuted(muteUntil);
}
listener.onMuted(muteUntil);
});
if (cancelListener != null) {

View File

@@ -21,23 +21,22 @@ import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.contacts.sync.ContactDiscovery;
import org.thoughtcrime.securesms.contacts.sync.DirectoryHelper;
import org.thoughtcrime.securesms.conversation.ConversationIntents;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.groups.ui.creategroup.CreateGroupActivity;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.signal.core.util.concurrent.SimpleTask;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
import org.whispersystems.libsignal.util.guava.Optional;
import java.io.IOException;
import java.util.Optional;
import java.util.function.Consumer;
/**
@@ -62,13 +61,13 @@ public class NewConversationActivity extends ContactSelectionActivity
}
@Override
public void onBeforeContactSelected(@NonNull Optional<RecipientId> recipientId, String number, @NonNull Consumer<Boolean> callback) {
public void onBeforeContactSelected(Optional<RecipientId> recipientId, String number, Consumer<Boolean> callback) {
if (recipientId.isPresent()) {
launch(Recipient.resolved(recipientId.get()));
} else {
Log.i(TAG, "[onContactSelected] Maybe creating a new recipient.");
if (SignalStore.account().isRegistered() && NetworkConstraint.isMet(getApplication())) {
if (TextSecurePreferences.isPushRegistered(this) && NetworkConstraint.isMet(this)) {
Log.i(TAG, "[onContactSelected] Doing contact refresh.");
AlertDialog progress = SimpleProgressDialog.show(this);
@@ -76,10 +75,10 @@ public class NewConversationActivity extends ContactSelectionActivity
SimpleTask.run(getLifecycle(), () -> {
Recipient resolved = Recipient.external(this, number);
if (!resolved.isRegistered() || !resolved.hasServiceId()) {
if (!resolved.isRegistered() || !resolved.hasUuid()) {
Log.i(TAG, "[onContactSelected] Not registered or no UUID. Doing a directory refresh.");
try {
ContactDiscovery.refresh(this, resolved, false);
DirectoryHelper.refreshDirectoryFor(this, resolved, false);
resolved = Recipient.resolved(resolved.getId());
} catch (IOException e) {
Log.w(TAG, "[onContactSelected] Failed to refresh directory for new contact.");
@@ -104,7 +103,7 @@ public class NewConversationActivity extends ContactSelectionActivity
}
private void launch(Recipient recipient) {
long existingThread = SignalDatabase.threads().getThreadIdIfExistsFor(recipient.getId());
long existingThread = DatabaseFactory.getThreadDatabase(this).getThreadIdIfExistsFor(recipient.getId());
Intent intent = ConversationIntents.createBuilder(this, recipient.getId(), existingThread)
.withDraftText(getIntent().getStringExtra(Intent.EXTRA_TEXT))
.withDataUri(getIntent().getData())

View File

@@ -1,40 +0,0 @@
package org.thoughtcrime.securesms
import android.graphics.Bitmap
import android.graphics.Canvas
import androidx.annotation.ColorInt
import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
import java.security.MessageDigest
/**
* BitmapTransformation which overlays the given bitmap with the given color.
*/
class OverlayTransformation(
@ColorInt private val color: Int
) : BitmapTransformation() {
private val id = "${OverlayTransformation::class.java.name}$color"
override fun updateDiskCacheKey(messageDigest: MessageDigest) {
messageDigest.update(id.toByteArray(CHARSET))
}
override fun transform(pool: BitmapPool, toTransform: Bitmap, outWidth: Int, outHeight: Int): Bitmap {
val outBitmap = pool.get(outWidth, outHeight, Bitmap.Config.ARGB_8888)
val canvas = Canvas(outBitmap)
canvas.drawBitmap(toTransform, 0f, 0f, null)
canvas.drawColor(color)
return outBitmap
}
override fun equals(other: Any?): Boolean {
return (other as? OverlayTransformation)?.color == color
}
override fun hashCode(): Int {
return id.hashCode()
}
}

View File

@@ -19,9 +19,9 @@ package org.thoughtcrime.securesms;
import android.os.AsyncTask;
import android.os.Bundle;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.MasterSecretUtil;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.util.VersionTracker;
/**
@@ -61,8 +61,7 @@ public class PassphraseCreateActivity extends PassphraseActivity {
passphrase);
MasterSecretUtil.generateAsymmetricMasterSecret(PassphraseCreateActivity.this, masterSecret);
SignalStore.account().generateAciIdentityKeyIfNecessary();
SignalStore.account().generatePniIdentityKeyIfNecessary();
IdentityKeyUtil.generateIdentityKeys(PassphraseCreateActivity.this);
VersionTracker.updateLastSeenVersion(PassphraseCreateActivity.this);
return null;

View File

@@ -60,7 +60,7 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
protected final void onCreate(Bundle savedInstanceState) {
Tracer.getInstance().start(Log.tag(getClass()) + "#onCreate()");
AppStartup.getInstance().onCriticalRenderEventStart();
this.networkAccess = ApplicationDependencies.getSignalServiceNetworkAccess();
this.networkAccess = new SignalServiceNetworkAccess(this);
onPreCreate();
final boolean locked = KeyCachingService.isLocked(this);
@@ -84,7 +84,7 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
protected void onResume() {
super.onResume();
if (networkAccess.isCensored()) {
if (networkAccess.isCensored(this)) {
ApplicationDependencies.getJobManager().add(new PushNotificationReceiveJob());
}
}
@@ -171,8 +171,6 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
return STATE_WELCOME_PUSH_SCREEN;
} else if (SignalStore.storageService().needsAccountRestore()) {
return STATE_ENTER_SIGNAL_PIN;
} else if (userHasSkippedOrForgottenPin()) {
return STATE_CREATE_SIGNAL_PIN;
} else if (userMustSetProfileName()) {
return STATE_CREATE_PROFILE_NAME;
} else if (userMustCreateSignalPin()) {
@@ -192,10 +190,6 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
return !SignalStore.registrationValues().isRegistrationComplete() && !SignalStore.kbsValues().hasPin() && !SignalStore.kbsValues().lastPinCreateFailed() && !SignalStore.kbsValues().hasOptedOut();
}
private boolean userHasSkippedOrForgottenPin() {
return !SignalStore.registrationValues().isRegistrationComplete() && !SignalStore.kbsValues().hasPin() && !SignalStore.kbsValues().hasOptedOut() && SignalStore.kbsValues().isPinForgottenOrSkipped();
}
private boolean userMustSetProfileName() {
return !SignalStore.registrationValues().isRegistrationComplete() && Recipient.self().getProfileName().isEmpty();
}

View File

@@ -0,0 +1,48 @@
package org.thoughtcrime.securesms;
import android.os.Build;
import java.util.HashSet;
import java.util.Set;
/**
* Device hardware capability lists.
* <p>
* Moved outside of ApplicationContext as the indirection was important for API19 support with desugaring: https://issuetracker.google.com/issues/183419297
*/
final class RtcDeviceLists {
private RtcDeviceLists() {}
static Set<String> hardwareAECBlockList() {
return new HashSet<String>() {{
add("Pixel");
add("Pixel XL");
add("Moto G5");
add("Moto G (5S) Plus");
add("Moto G4");
add("TA-1053");
add("Mi A1");
add("Mi A2");
add("E5823"); // Sony z5 compact
add("Redmi Note 5");
add("FP2"); // Fairphone FP2
add("MI 5");
}};
}
static Set<String> openSlEsAllowList() {
return new HashSet<String>() {{
add("Pixel");
add("Pixel XL");
}};
}
static boolean hardwareAECBlocked() {
return hardwareAECBlockList().contains(Build.MODEL);
}
static boolean openSLESAllowed() {
return openSlEsAllowList().contains(Build.MODEL);
}
}

View File

@@ -12,7 +12,7 @@ import androidx.annotation.NonNull;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.conversation.ConversationIntents;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.Rfc5724Uri;
@@ -48,7 +48,7 @@ public class SmsSendtoActivity extends Activity {
Toast.makeText(this, R.string.ConversationActivity_specify_recipient, Toast.LENGTH_LONG).show();
} else {
Recipient recipient = Recipient.external(this, destination.getDestination());
long threadId = SignalDatabase.threads().getThreadIdIfExistsFor(recipient.getId());
long threadId = DatabaseFactory.getThreadDatabase(this).getThreadIdIfExistsFor(recipient.getId());
nextIntent = ConversationIntents.createBuilder(this, recipient.getId(), threadId)
.withDraftText(destination.getBody())

View File

@@ -0,0 +1,147 @@
package org.thoughtcrime.securesms;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.TextUtils;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.util.CharacterCalculator;
import org.thoughtcrime.securesms.util.CharacterCalculator.CharacterState;
import org.whispersystems.libsignal.util.guava.Optional;
public class TransportOption implements Parcelable {
public enum Type {
SMS,
TEXTSECURE
}
private final int drawable;
private final int backgroundColor;
private final @NonNull String text;
private final @NonNull Type type;
private final @NonNull String composeHint;
private final @NonNull CharacterCalculator characterCalculator;
private final @NonNull Optional<CharSequence> simName;
private final @NonNull Optional<Integer> simSubscriptionId;
public TransportOption(@NonNull Type type,
@DrawableRes int drawable,
int backgroundColor,
@NonNull String text,
@NonNull String composeHint,
@NonNull CharacterCalculator characterCalculator)
{
this(type, drawable, backgroundColor, text, composeHint, characterCalculator,
Optional.<CharSequence>absent(), Optional.<Integer>absent());
}
public TransportOption(@NonNull Type type,
@DrawableRes int drawable,
int backgroundColor,
@NonNull String text,
@NonNull String composeHint,
@NonNull CharacterCalculator characterCalculator,
@NonNull Optional<CharSequence> simName,
@NonNull Optional<Integer> simSubscriptionId)
{
this.type = type;
this.drawable = drawable;
this.backgroundColor = backgroundColor;
this.text = text;
this.composeHint = composeHint;
this.characterCalculator = characterCalculator;
this.simName = simName;
this.simSubscriptionId = simSubscriptionId;
}
TransportOption(Parcel in) {
this(Type.valueOf(in.readString()),
in.readInt(),
in.readInt(),
in.readString(),
in.readString(),
CharacterCalculator.readFromParcel(in),
Optional.fromNullable(TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in)),
in.readInt() == 1 ? Optional.of(in.readInt()) : Optional.absent());
}
public @NonNull Type getType() {
return type;
}
public boolean isType(Type type) {
return this.type == type;
}
public boolean isSms() {
return type == Type.SMS;
}
public CharacterState calculateCharacters(String messageBody) {
return characterCalculator.calculateCharacters(messageBody);
}
public @DrawableRes int getDrawable() {
return drawable;
}
public int getBackgroundColor() {
return backgroundColor;
}
public @NonNull String getComposeHint() {
return composeHint;
}
public @NonNull String getDescription() {
return text;
}
@NonNull
public Optional<CharSequence> getSimName() {
return simName;
}
@NonNull
public Optional<Integer> getSimSubscriptionId() {
return simSubscriptionId;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(type.name());
dest.writeInt(drawable);
dest.writeInt(backgroundColor);
dest.writeString(text);
dest.writeString(composeHint);
CharacterCalculator.writeToParcel(dest, characterCalculator);
TextUtils.writeToParcel(simName.orNull(), dest, flags);
if (simSubscriptionId.isPresent()) {
dest.writeInt(1);
dest.writeInt(simSubscriptionId.get());
} else {
dest.writeInt(0);
}
}
public static final Creator<TransportOption> CREATOR = new Creator<TransportOption>() {
@Override
public TransportOption createFromParcel(Parcel in) {
return new TransportOption(in);
}
@Override
public TransportOption[] newArray(int size) {
return new TransportOption[size];
}
};
}

View File

@@ -0,0 +1,225 @@
package org.thoughtcrime.securesms;
import android.Manifest;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.util.CharacterCalculator;
import org.thoughtcrime.securesms.util.MmsCharacterCalculator;
import org.thoughtcrime.securesms.util.PushCharacterCalculator;
import org.thoughtcrime.securesms.util.SmsCharacterCalculator;
import org.thoughtcrime.securesms.util.dualsim.SubscriptionInfoCompat;
import org.thoughtcrime.securesms.util.dualsim.SubscriptionManagerCompat;
import org.whispersystems.libsignal.util.guava.Optional;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import static org.thoughtcrime.securesms.TransportOption.Type;
public class TransportOptions {
private static final String TAG = Log.tag(TransportOptions.class);
private final List<OnTransportChangedListener> listeners = new LinkedList<>();
private final Context context;
private final List<TransportOption> enabledTransports;
private Type defaultTransportType = Type.SMS;
private Optional<Integer> defaultSubscriptionId = Optional.absent();
private Optional<TransportOption> selectedOption = Optional.absent();
private final Optional<Integer> systemSubscriptionId;
public TransportOptions(Context context, boolean media) {
this.context = context;
this.enabledTransports = initializeAvailableTransports(media);
this.systemSubscriptionId = new SubscriptionManagerCompat(context).getPreferredSubscriptionId();
}
public void reset(boolean media) {
List<TransportOption> transportOptions = initializeAvailableTransports(media);
this.enabledTransports.clear();
this.enabledTransports.addAll(transportOptions);
if (selectedOption.isPresent() && !isEnabled(selectedOption.get())) {
setSelectedTransport(null);
} else {
this.defaultTransportType = Type.SMS;
this.defaultSubscriptionId = Optional.absent();
notifyTransportChangeListeners();
}
}
public void setDefaultTransport(Type type) {
this.defaultTransportType = type;
if (!selectedOption.isPresent()) {
notifyTransportChangeListeners();
}
}
public void setDefaultSubscriptionId(Optional<Integer> subscriptionId) {
if (defaultSubscriptionId.equals(subscriptionId)) {
return;
}
this.defaultSubscriptionId = subscriptionId;
if (!selectedOption.isPresent()) {
notifyTransportChangeListeners();
}
}
public void setSelectedTransport(@Nullable TransportOption transportOption) {
this.selectedOption = Optional.fromNullable(transportOption);
notifyTransportChangeListeners();
}
public boolean isManualSelection() {
return this.selectedOption.isPresent();
}
public @NonNull TransportOption getSelectedTransport() {
if (selectedOption.isPresent()) return selectedOption.get();
if (defaultTransportType == Type.SMS) {
TransportOption transportOption = findEnabledSmsTransportOption(defaultSubscriptionId.or(systemSubscriptionId));
if (transportOption != null) {
return transportOption;
}
}
for (TransportOption transportOption : enabledTransports) {
if (transportOption.getType() == defaultTransportType) {
return transportOption;
}
}
throw new AssertionError("No options of default type!");
}
public static @NonNull TransportOption getPushTransportOption(@NonNull Context context) {
return new TransportOption(Type.TEXTSECURE,
R.drawable.ic_send_lock_24,
context.getResources().getColor(R.color.core_ultramarine),
context.getString(R.string.ConversationActivity_transport_signal),
context.getString(R.string.conversation_activity__type_message_push),
new PushCharacterCalculator());
}
private @Nullable TransportOption findEnabledSmsTransportOption(Optional<Integer> subscriptionId) {
if (subscriptionId.isPresent()) {
final int subId = subscriptionId.get();
for (TransportOption transportOption : enabledTransports) {
if (transportOption.getType() == Type.SMS &&
subId == transportOption.getSimSubscriptionId().or(-1)) {
return transportOption;
}
}
}
return null;
}
public void disableTransport(Type type) {
TransportOption selected = selectedOption.orNull();
Iterator<TransportOption> iterator = enabledTransports.iterator();
while (iterator.hasNext()) {
TransportOption option = iterator.next();
if (option.isType(type)) {
if (selected == option) {
setSelectedTransport(null);
}
iterator.remove();
}
}
}
public List<TransportOption> getEnabledTransports() {
return enabledTransports;
}
public void addOnTransportChangedListener(OnTransportChangedListener listener) {
this.listeners.add(listener);
}
private List<TransportOption> initializeAvailableTransports(boolean isMediaMessage) {
List<TransportOption> results = new LinkedList<>();
if (isMediaMessage) {
results.addAll(getTransportOptionsForSimCards(context.getString(R.string.ConversationActivity_transport_insecure_mms),
context.getString(R.string.conversation_activity__type_message_mms_insecure),
new MmsCharacterCalculator()));
} else {
results.addAll(getTransportOptionsForSimCards(context.getString(R.string.ConversationActivity_transport_insecure_sms),
context.getString(R.string.conversation_activity__type_message_sms_insecure),
new SmsCharacterCalculator()));
}
results.add(getPushTransportOption(context));
return results;
}
private @NonNull List<TransportOption> getTransportOptionsForSimCards(@NonNull String text,
@NonNull String composeHint,
@NonNull CharacterCalculator characterCalculator)
{
List<TransportOption> results = new LinkedList<>();
SubscriptionManagerCompat subscriptionManager = new SubscriptionManagerCompat(context);
Collection<SubscriptionInfoCompat> subscriptions;
if (Permissions.hasAll(context, Manifest.permission.READ_PHONE_STATE)) {
subscriptions = subscriptionManager.getActiveAndReadySubscriptionInfos();
} else {
subscriptions = Collections.emptyList();
}
if (subscriptions.size() < 2) {
results.add(new TransportOption(Type.SMS, R.drawable.ic_send_unlock_24,
context.getResources().getColor(R.color.core_grey_50),
text, composeHint, characterCalculator));
} else {
for (SubscriptionInfoCompat subscriptionInfo : subscriptions) {
results.add(new TransportOption(Type.SMS, R.drawable.ic_send_unlock_24,
context.getResources().getColor(R.color.core_grey_50),
text, composeHint, characterCalculator,
Optional.of(subscriptionInfo.getDisplayName()),
Optional.of(subscriptionInfo.getSubscriptionId())));
}
}
return results;
}
private void notifyTransportChangeListeners() {
for (OnTransportChangedListener listener : listeners) {
listener.onChange(getSelectedTransport(), selectedOption.isPresent());
}
}
private boolean isEnabled(TransportOption transportOption) {
for (TransportOption option : enabledTransports) {
if (option.equals(transportOption)) return true;
}
return false;
}
public interface OnTransportChangedListener {
public void onChange(TransportOption newTransport, boolean manuallySelected);
}
}

View File

@@ -0,0 +1,73 @@
package org.thoughtcrime.securesms;
import android.content.Context;
import android.graphics.PorterDuff.Mode;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import java.util.List;
public class TransportOptionsAdapter extends BaseAdapter {
private final LayoutInflater inflater;
private List<TransportOption> enabledTransports;
public TransportOptionsAdapter(@NonNull Context context,
@NonNull List<TransportOption> enabledTransports)
{
super();
this.inflater = LayoutInflater.from(context);
this.enabledTransports = enabledTransports;
}
public void setEnabledTransports(List<TransportOption> enabledTransports) {
this.enabledTransports = enabledTransports;
}
@Override
public int getCount() {
return enabledTransports.size();
}
@Override
public Object getItem(int position) {
return enabledTransports.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
convertView = inflater.inflate(R.layout.transport_selection_list_item, parent, false);
}
TransportOption transport = (TransportOption) getItem(position);
ImageView imageView = convertView.findViewById(R.id.icon);
TextView textView = convertView.findViewById(R.id.text);
TextView subtextView = convertView.findViewById(R.id.subtext);
imageView.getBackground().setColorFilter(transport.getBackgroundColor(), Mode.MULTIPLY);
imageView.setImageResource(transport.getDrawable());
textView.setText(transport.getDescription());
if (transport.getSimName().isPresent()) {
subtextView.setText(transport.getSimName().get());
subtextView.setVisibility(View.VISIBLE);
} else {
subtextView.setVisibility(View.GONE);
}
return convertView;
}
}

View File

@@ -0,0 +1,50 @@
package org.thoughtcrime.securesms;
import android.content.Context;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ListView;
import androidx.annotation.NonNull;
import androidx.appcompat.widget.ListPopupWindow;
import java.util.LinkedList;
import java.util.List;
public class TransportOptionsPopup extends ListPopupWindow implements ListView.OnItemClickListener {
private final TransportOptionsAdapter adapter;
private final SelectedListener listener;
public TransportOptionsPopup(@NonNull Context context, @NonNull View anchor, @NonNull SelectedListener listener) {
super(context);
this.listener = listener;
this.adapter = new TransportOptionsAdapter(context, new LinkedList<TransportOption>());
setVerticalOffset(context.getResources().getDimensionPixelOffset(R.dimen.transport_selection_popup_yoff));
setHorizontalOffset(context.getResources().getDimensionPixelOffset(R.dimen.transport_selection_popup_xoff));
setInputMethodMode(ListPopupWindow.INPUT_METHOD_NOT_NEEDED);
setModal(true);
setAnchorView(anchor);
setAdapter(adapter);
setContentWidth(context.getResources().getDimensionPixelSize(R.dimen.transport_selection_popup_width));
setOnItemClickListener(this);
}
public void display(List<TransportOption> enabledTransports) {
adapter.setEnabledTransports(enabledTransports);
adapter.notifyDataSetChanged();
show();
}
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
listener.onSelected((TransportOption)adapter.getItem(position));
}
public interface SelectedListener {
void onSelected(TransportOption option);
}
}

View File

@@ -0,0 +1,770 @@
/*
* Copyright (C) 2016-2017 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms;
import android.Manifest;
import android.animation.TypeEvaluator;
import android.animation.ValueAnimator;
import android.annotation.SuppressLint;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.PorterDuff;
import android.graphics.drawable.BitmapDrawable;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Vibrator;
import android.text.Html;
import android.text.TextUtils;
import android.text.method.LinkMovementMethod;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.animation.Animation;
import android.view.animation.AnticipateInterpolator;
import android.view.animation.ScaleAnimation;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.ScrollView;
import android.widget.TextSwitcher;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import androidx.core.view.OneShotPreDrawListener;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentTransaction;
import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
import org.signal.core.util.ThreadUtil;
import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.components.ShapeScrim;
import org.thoughtcrime.securesms.components.camera.CameraView;
import org.thoughtcrime.securesms.crypto.ReentrantSessionLock;
import org.thoughtcrime.securesms.crypto.IdentityKeyParcelable;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
import org.thoughtcrime.securesms.database.IdentityDatabase;
import org.thoughtcrime.securesms.database.IdentityDatabase.VerifiedStatus;
import org.thoughtcrime.securesms.database.model.IdentityRecord;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobs.MultiDeviceVerifiedUpdateJob;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.qr.QrCode;
import org.thoughtcrime.securesms.qr.ScanListener;
import org.thoughtcrime.securesms.qr.ScanningThread;
import org.thoughtcrime.securesms.recipients.LiveRecipient;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.IdentityUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.fingerprint.Fingerprint;
import org.whispersystems.libsignal.fingerprint.FingerprintVersionMismatchException;
import org.whispersystems.libsignal.fingerprint.NumericFingerprintGenerator;
import org.whispersystems.signalservice.api.SignalSessionLock;
import org.whispersystems.signalservice.api.util.UuidUtil;
import java.nio.charset.Charset;
import java.util.Locale;
/**
* Activity for verifying identity keys.
*
* @author Moxie Marlinspike
*/
@SuppressLint("StaticFieldLeak")
public class VerifyIdentityActivity extends PassphraseRequiredActivity implements ScanListener, View.OnClickListener {
private static final String TAG = Log.tag(VerifyIdentityActivity.class);
private static final String RECIPIENT_EXTRA = "recipient_id";
private static final String IDENTITY_EXTRA = "recipient_identity";
private static final String VERIFIED_EXTRA = "verified_state";
private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
private final VerifyDisplayFragment displayFragment = new VerifyDisplayFragment();
private final VerifyScanFragment scanFragment = new VerifyScanFragment();
public static Intent newIntent(@NonNull Context context,
@NonNull IdentityRecord identityRecord)
{
return newIntent(context,
identityRecord.getRecipientId(),
identityRecord.getIdentityKey(),
identityRecord.getVerifiedStatus() == IdentityDatabase.VerifiedStatus.VERIFIED);
}
public static Intent newIntent(@NonNull Context context,
@NonNull IdentityRecord identityRecord,
boolean verified)
{
return newIntent(context,
identityRecord.getRecipientId(),
identityRecord.getIdentityKey(),
verified);
}
public static Intent newIntent(@NonNull Context context,
@NonNull RecipientId recipientId,
@NonNull IdentityKey identityKey,
boolean verified)
{
Intent intent = new Intent(context, VerifyIdentityActivity.class);
intent.putExtra(RECIPIENT_EXTRA, recipientId);
intent.putExtra(IDENTITY_EXTRA, new IdentityKeyParcelable(identityKey));
intent.putExtra(VERIFIED_EXTRA, verified);
return intent;
}
@Override
public void onPreCreate() {
dynamicTheme.onCreate(this);
}
@Override
protected void onCreate(Bundle state, boolean ready) {
Bundle extras = new Bundle();
extras.putParcelable(VerifyDisplayFragment.RECIPIENT_ID, getIntent().getParcelableExtra(RECIPIENT_EXTRA));
extras.putParcelable(VerifyDisplayFragment.REMOTE_IDENTITY, getIntent().getParcelableExtra(IDENTITY_EXTRA));
extras.putParcelable(VerifyDisplayFragment.LOCAL_IDENTITY, new IdentityKeyParcelable(IdentityKeyUtil.getIdentityKey(this)));
extras.putString(VerifyDisplayFragment.LOCAL_NUMBER, TextSecurePreferences.getLocalNumber(this));
extras.putBoolean(VerifyDisplayFragment.VERIFIED_STATE, getIntent().getBooleanExtra(VERIFIED_EXTRA, false));
scanFragment.setScanListener(this);
displayFragment.setClickListener(this);
initFragment(android.R.id.content, displayFragment, Locale.getDefault(), extras);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home: finish(); return true;
}
return false;
}
@Override
public void onQrDataFound(final String data) {
ThreadUtil.runOnMain(() -> {
((Vibrator)getSystemService(Context.VIBRATOR_SERVICE)).vibrate(50);
getSupportFragmentManager().popBackStack();
displayFragment.setScannedFingerprint(data);
});
}
@Override
public void onClick(View v) {
Permissions.with(this)
.request(Manifest.permission.CAMERA)
.ifNecessary()
.withPermanentDenialDialog(getString(R.string.VerifyIdentityActivity_signal_needs_the_camera_permission_in_order_to_scan_a_qr_code_but_it_has_been_permanently_denied))
.onAllGranted(() -> {
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
transaction.setCustomAnimations(R.anim.slide_from_top, R.anim.slide_to_bottom,
R.anim.slide_from_bottom, R.anim.slide_to_top);
transaction.replace(android.R.id.content, scanFragment)
.addToBackStack(null)
.commitAllowingStateLoss();
})
.onAnyDenied(() -> Toast.makeText(this, R.string.VerifyIdentityActivity_unable_to_scan_qr_code_without_camera_permission, Toast.LENGTH_LONG).show())
.execute();
}
@SuppressLint("MissingSuperCall")
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
}
public static class VerifyDisplayFragment extends Fragment implements ViewTreeObserver.OnScrollChangedListener {
public static final String RECIPIENT_ID = "recipient_id";
public static final String REMOTE_NUMBER = "remote_number";
public static final String REMOTE_IDENTITY = "remote_identity";
public static final String LOCAL_IDENTITY = "local_identity";
public static final String LOCAL_NUMBER = "local_number";
public static final String VERIFIED_STATE = "verified_state";
private LiveRecipient recipient;
private IdentityKey localIdentity;
private IdentityKey remoteIdentity;
private Fingerprint fingerprint;
private Toolbar toolbar;
private ScrollView scrollView;
private View container;
private View numbersContainer;
private View loading;
private View qrCodeContainer;
private ImageView qrCode;
private ImageView qrVerified;
private TextSwitcher tapLabel;
private TextView description;
private View.OnClickListener clickListener;
private Button verifyButton;
private View toolbarShadow;
private View bottomShadow;
private TextView[] codes = new TextView[12];
private boolean animateSuccessOnDraw = false;
private boolean animateFailureOnDraw = false;
private boolean currentVerifiedState = false;
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup viewGroup, Bundle bundle) {
this.container = ViewUtil.inflate(inflater, viewGroup, R.layout.verify_display_fragment);
this.toolbar = container.findViewById(R.id.toolbar);
this.scrollView = container.findViewById(R.id.scroll_view);
this.numbersContainer = container.findViewById(R.id.number_table);
this.loading = container.findViewById(R.id.loading);
this.qrCodeContainer = container.findViewById(R.id.qr_code_container);
this.qrCode = container.findViewById(R.id.qr_code);
this.verifyButton = container.findViewById(R.id.verify_button);
this.qrVerified = container.findViewById(R.id.qr_verified);
this.description = container.findViewById(R.id.description);
this.tapLabel = container.findViewById(R.id.tap_label);
this.toolbarShadow = container.findViewById(R.id.toolbar_shadow);
this.bottomShadow = container.findViewById(R.id.verify_identity_bottom_shadow);
this.codes[0] = container.findViewById(R.id.code_first);
this.codes[1] = container.findViewById(R.id.code_second);
this.codes[2] = container.findViewById(R.id.code_third);
this.codes[3] = container.findViewById(R.id.code_fourth);
this.codes[4] = container.findViewById(R.id.code_fifth);
this.codes[5] = container.findViewById(R.id.code_sixth);
this.codes[6] = container.findViewById(R.id.code_seventh);
this.codes[7] = container.findViewById(R.id.code_eighth);
this.codes[8] = container.findViewById(R.id.code_ninth);
this.codes[9] = container.findViewById(R.id.code_tenth);
this.codes[10] = container.findViewById(R.id.code_eleventh);
this.codes[11] = container.findViewById(R.id.code_twelth);
this.qrCodeContainer.setOnClickListener(clickListener);
this.registerForContextMenu(numbersContainer);
updateVerifyButton(getArguments().getBoolean(VERIFIED_STATE, false), false);
this.verifyButton.setOnClickListener((button -> updateVerifyButton(!currentVerifiedState, true)));
this.scrollView.getViewTreeObserver().addOnScrollChangedListener(this);
((AppCompatActivity)requireActivity()).setSupportActionBar(toolbar);
((AppCompatActivity)requireActivity()).setTitle(R.string.AndroidManifest__verify_safety_number);
return container;
}
@Override public void onDestroyView() {
this.scrollView.getViewTreeObserver().removeOnScrollChangedListener(this);
super.onDestroyView();
}
@Override
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
RecipientId recipientId = getArguments().getParcelable(RECIPIENT_ID);
IdentityKeyParcelable localIdentityParcelable = getArguments().getParcelable(LOCAL_IDENTITY);
IdentityKeyParcelable remoteIdentityParcelable = getArguments().getParcelable(REMOTE_IDENTITY);
if (recipientId == null) throw new AssertionError("RecipientId required");
if (localIdentityParcelable == null) throw new AssertionError("local identity required");
if (remoteIdentityParcelable == null) throw new AssertionError("remote identity required");
this.localIdentity = localIdentityParcelable.get();
this.recipient = Recipient.live(recipientId);
this.remoteIdentity = remoteIdentityParcelable.get();
int version;
byte[] localId;
byte[] remoteId;
//noinspection WrongThread
Recipient resolved = recipient.resolve();
if (FeatureFlags.verifyV2() && resolved.getUuid().isPresent()) {
Log.i(TAG, "Using UUID (version 2).");
version = 2;
localId = UuidUtil.toByteArray(TextSecurePreferences.getLocalUuid(requireContext()));
remoteId = UuidUtil.toByteArray(resolved.getUuid().get());
} else if (!FeatureFlags.verifyV2() && resolved.getE164().isPresent()) {
Log.i(TAG, "Using E164 (version 1).");
version = 1;
localId = TextSecurePreferences.getLocalNumber(requireContext()).getBytes();
remoteId = resolved.requireE164().getBytes();
} else {
Log.w(TAG, String.format(Locale.ENGLISH, "Could not show proper verification! verifyV2: %s, hasUuid: %s, hasE164: %s", FeatureFlags.verifyV2(), resolved.getUuid().isPresent(), resolved.getE164().isPresent()));
new AlertDialog.Builder(requireContext())
.setMessage(getString(R.string.VerifyIdentityActivity_you_must_first_exchange_messages_in_order_to_view, resolved.getDisplayName(requireContext())))
.setPositiveButton(android.R.string.ok, (dialog, which) -> requireActivity().finish())
.setOnDismissListener(dialog -> requireActivity().finish())
.show();
return;
}
this.recipient.observe(this, this::setRecipientText);
new AsyncTask<Void, Void, Fingerprint>() {
@Override
protected Fingerprint doInBackground(Void... params) {
return new NumericFingerprintGenerator(5200).createFor(version,
localId, localIdentity,
remoteId, remoteIdentity);
}
@Override
protected void onPostExecute(Fingerprint fingerprint) {
if (getActivity() == null) return;
VerifyDisplayFragment.this.fingerprint = fingerprint;
setFingerprintViews(fingerprint, true);
getActivity().supportInvalidateOptionsMenu();
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
setHasOptionsMenu(true);
}
@Override
public void onResume() {
super.onResume();
setRecipientText(recipient.get());
if (fingerprint != null) {
setFingerprintViews(fingerprint, false);
}
if (animateSuccessOnDraw) {
animateSuccessOnDraw = false;
animateVerifiedSuccess();
} else if (animateFailureOnDraw) {
animateFailureOnDraw = false;
animateVerifiedFailure();
}
ThreadUtil.postToMain(this::onScrollChanged);
}
@Override
public void onCreateContextMenu(ContextMenu menu, View view,
ContextMenuInfo menuInfo)
{
super.onCreateContextMenu(menu, view, menuInfo);
if (fingerprint != null) {
MenuInflater inflater = getActivity().getMenuInflater();
inflater.inflate(R.menu.verify_display_fragment_context_menu, menu);
}
}
@Override
public boolean onContextItemSelected(MenuItem item) {
if (fingerprint == null) return super.onContextItemSelected(item);
switch (item.getItemId()) {
case R.id.menu_copy: handleCopyToClipboard(fingerprint, codes.length); return true;
case R.id.menu_compare: handleCompareWithClipboard(fingerprint); return true;
default: return super.onContextItemSelected(item);
}
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
if (fingerprint != null) {
inflater.inflate(R.menu.verify_identity, menu);
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.verify_identity__share: handleShare(fingerprint, codes.length); return true;
}
return false;
}
public void setScannedFingerprint(String scanned) {
try {
if (fingerprint.getScannableFingerprint().compareTo(scanned.getBytes("ISO-8859-1"))) {
this.animateSuccessOnDraw = true;
} else {
this.animateFailureOnDraw = true;
}
} catch (FingerprintVersionMismatchException e) {
Log.w(TAG, e);
if (e.getOurVersion() < e.getTheirVersion()) {
Toast.makeText(getActivity(), R.string.VerifyIdentityActivity_your_contact_is_running_a_newer_version_of_Signal, Toast.LENGTH_LONG).show();
} else {
Toast.makeText(getActivity(), R.string.VerifyIdentityActivity_your_contact_is_running_an_old_version_of_signal, Toast.LENGTH_LONG).show();
}
this.animateFailureOnDraw = true;
} catch (Exception e) {
Log.w(TAG, e);
Toast.makeText(getActivity(), R.string.VerifyIdentityActivity_the_scanned_qr_code_is_not_a_correctly_formatted_safety_number, Toast.LENGTH_LONG).show();
this.animateFailureOnDraw = true;
}
}
public void setClickListener(View.OnClickListener listener) {
this.clickListener = listener;
}
private @NonNull String getFormattedSafetyNumbers(@NonNull Fingerprint fingerprint, int segmentCount) {
String[] segments = getSegments(fingerprint, segmentCount);
StringBuilder result = new StringBuilder();
for (int i = 0; i < segments.length; i++) {
result.append(segments[i]);
if (i != segments.length - 1) {
if (((i+1) % 4) == 0) result.append('\n');
else result.append(' ');
}
}
return result.toString();
}
private void handleCopyToClipboard(Fingerprint fingerprint, int segmentCount) {
Util.writeTextToClipboard(getActivity(), getFormattedSafetyNumbers(fingerprint, segmentCount));
}
private void handleCompareWithClipboard(Fingerprint fingerprint) {
String clipboardData = Util.readTextFromClipboard(getActivity());
if (clipboardData == null) {
Toast.makeText(getActivity(), R.string.VerifyIdentityActivity_no_safety_number_to_compare_was_found_in_the_clipboard, Toast.LENGTH_LONG).show();
return;
}
String numericClipboardData = clipboardData.replaceAll("\\D", "");
if (TextUtils.isEmpty(numericClipboardData) || numericClipboardData.length() != 60) {
Toast.makeText(getActivity(), R.string.VerifyIdentityActivity_no_safety_number_to_compare_was_found_in_the_clipboard, Toast.LENGTH_LONG).show();
return;
}
if (fingerprint.getDisplayableFingerprint().getDisplayText().equals(numericClipboardData)) {
animateVerifiedSuccess();
} else {
animateVerifiedFailure();
}
}
private void handleShare(@NonNull Fingerprint fingerprint, int segmentCount) {
String shareString =
getString(R.string.VerifyIdentityActivity_our_signal_safety_number) + "\n" +
getFormattedSafetyNumbers(fingerprint, segmentCount) + "\n";
Intent intent = new Intent();
intent.setAction(Intent.ACTION_SEND);
intent.putExtra(Intent.EXTRA_TEXT, shareString);
intent.setType("text/plain");
try {
startActivity(Intent.createChooser(intent, getString(R.string.VerifyIdentityActivity_share_safety_number_via)));
} catch (ActivityNotFoundException e) {
Toast.makeText(getActivity(), R.string.VerifyIdentityActivity_no_app_to_share_to, Toast.LENGTH_LONG).show();
}
}
private void setRecipientText(Recipient recipient) {
description.setText(Html.fromHtml(String.format(getActivity().getString(R.string.verify_display_fragment__to_verify_the_security_of_your_end_to_end_encryption_with_s), recipient.getDisplayName(getContext()))));
description.setMovementMethod(LinkMovementMethod.getInstance());
}
private void setFingerprintViews(Fingerprint fingerprint, boolean animate) {
String[] segments = getSegments(fingerprint, codes.length);
for (int i=0;i<codes.length;i++) {
if (animate) setCodeSegment(codes[i], segments[i]);
else codes[i].setText(segments[i]);
}
byte[] qrCodeData = fingerprint.getScannableFingerprint().getSerialized();
String qrCodeString = new String(qrCodeData, Charset.forName("ISO-8859-1"));
Bitmap qrCodeBitmap = QrCode.create(qrCodeString);
qrCode.setImageBitmap(qrCodeBitmap);
if (animate) {
ViewUtil.fadeIn(qrCode, 1000);
ViewUtil.fadeIn(tapLabel, 1000);
ViewUtil.fadeOut(loading, 300, View.GONE);
} else {
qrCode.setVisibility(View.VISIBLE);
tapLabel.setVisibility(View.VISIBLE);
loading.setVisibility(View.GONE);
}
}
private void setCodeSegment(final TextView codeView, String segment) {
ValueAnimator valueAnimator = new ValueAnimator();
valueAnimator.setObjectValues(0, Integer.parseInt(segment));
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
int value = (int) animation.getAnimatedValue();
codeView.setText(String.format(Locale.getDefault(), "%05d", value));
}
});
valueAnimator.setEvaluator(new TypeEvaluator<Integer>() {
public Integer evaluate(float fraction, Integer startValue, Integer endValue) {
return Math.round(startValue + (endValue - startValue) * fraction);
}
});
valueAnimator.setDuration(1000);
valueAnimator.start();
}
private String[] getSegments(Fingerprint fingerprint, int segmentCount) {
String[] segments = new String[segmentCount];
String digits = fingerprint.getDisplayableFingerprint().getDisplayText();
int partSize = digits.length() / segmentCount;
for (int i=0;i<segmentCount;i++) {
segments[i] = digits.substring(i * partSize, (i * partSize) + partSize);
}
return segments;
}
private Bitmap createVerifiedBitmap(int width, int height, @DrawableRes int id) {
Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
Bitmap check = BitmapFactory.decodeResource(getResources(), id);
float offset = (width - check.getWidth()) / 2;
canvas.drawBitmap(check, offset, offset, null);
return bitmap;
}
private void animateVerifiedSuccess() {
Bitmap qrBitmap = ((BitmapDrawable)qrCode.getDrawable()).getBitmap();
Bitmap qrSuccess = createVerifiedBitmap(qrBitmap.getWidth(), qrBitmap.getHeight(), R.drawable.ic_check_white_48dp);
qrVerified.setImageBitmap(qrSuccess);
qrVerified.getBackground().setColorFilter(getResources().getColor(R.color.green_500), PorterDuff.Mode.MULTIPLY);
tapLabel.setText(getString(R.string.verify_display_fragment__successful_match));
animateVerified();
}
private void animateVerifiedFailure() {
Bitmap qrBitmap = ((BitmapDrawable)qrCode.getDrawable()).getBitmap();
Bitmap qrSuccess = createVerifiedBitmap(qrBitmap.getWidth(), qrBitmap.getHeight(), R.drawable.ic_close_white_48dp);
qrVerified.setImageBitmap(qrSuccess);
qrVerified.getBackground().setColorFilter(getResources().getColor(R.color.red_500), PorterDuff.Mode.MULTIPLY);
tapLabel.setText(getString(R.string.verify_display_fragment__failed_to_verify_safety_number));
animateVerified();
}
private void animateVerified() {
ScaleAnimation scaleAnimation = new ScaleAnimation(0, 1, 0, 1,
ScaleAnimation.RELATIVE_TO_SELF, 0.5f,
ScaleAnimation.RELATIVE_TO_SELF, 0.5f);
scaleAnimation.setInterpolator(new FastOutSlowInInterpolator());
scaleAnimation.setDuration(800);
scaleAnimation.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {}
@Override
public void onAnimationEnd(Animation animation) {
qrVerified.postDelayed(new Runnable() {
@Override
public void run() {
ScaleAnimation scaleAnimation = new ScaleAnimation(1, 0, 1, 0,
ScaleAnimation.RELATIVE_TO_SELF, 0.5f,
ScaleAnimation.RELATIVE_TO_SELF, 0.5f);
scaleAnimation.setInterpolator(new AnticipateInterpolator());
scaleAnimation.setDuration(500);
ViewUtil.animateOut(qrVerified, scaleAnimation, View.GONE);
ViewUtil.fadeIn(qrCode, 800);
qrCodeContainer.setEnabled(true);
tapLabel.setText(getString(R.string.verify_display_fragment__tap_to_scan));
}
}, 2000);
}
@Override
public void onAnimationRepeat(Animation animation) {}
});
ViewUtil.fadeOut(qrCode, 200, View.INVISIBLE);
ViewUtil.animateIn(qrVerified, scaleAnimation);
qrCodeContainer.setEnabled(false);
}
private void updateVerifyButton(boolean verified, boolean update) {
currentVerifiedState = verified;
if (verified) {
verifyButton.setText(R.string.verify_display_fragment__clear_verification);
} else {
verifyButton.setText(R.string.verify_display_fragment__mark_as_verified);
}
if (update) {
final RecipientId recipientId = recipient.getId();
SignalExecutors.BOUNDED.execute(() -> {
try (SignalSessionLock.Lock unused = ReentrantSessionLock.INSTANCE.acquire()) {
if (verified) {
Log.i(TAG, "Saving identity: " + recipientId);
ApplicationDependencies.getIdentityStore()
.saveIdentityWithoutSideEffects(recipientId,
remoteIdentity,
VerifiedStatus.VERIFIED,
false,
System.currentTimeMillis(),
true);
} else {
ApplicationDependencies.getIdentityStore().setVerified(recipientId, remoteIdentity, VerifiedStatus.DEFAULT);
}
ApplicationDependencies.getJobManager()
.add(new MultiDeviceVerifiedUpdateJob(recipientId,
remoteIdentity,
verified ? VerifiedStatus.VERIFIED
: VerifiedStatus.DEFAULT));
StorageSyncHelper.scheduleSyncForDataChange();
IdentityUtil.markIdentityVerified(getActivity(), recipient.get(), verified, false);
}
});
}
}
@Override public void onScrollChanged() {
if (scrollView.canScrollVertically(-1)) {
if (toolbarShadow.getVisibility() != View.VISIBLE) {
ViewUtil.fadeIn(toolbarShadow, 250);
}
} else {
if (toolbarShadow.getVisibility() != View.GONE) {
ViewUtil.fadeOut(toolbarShadow, 250);
}
}
if (scrollView.canScrollVertically(1)) {
if (bottomShadow.getVisibility() != View.VISIBLE) {
ViewUtil.fadeIn(bottomShadow, 250);
}
} else {
ViewUtil.fadeOut(bottomShadow, 250);
}
}
}
public static class VerifyScanFragment extends Fragment {
private View container;
private CameraView cameraView;
private ShapeScrim cameraScrim;
private ImageView cameraMarks;
private ScanningThread scanningThread;
private ScanListener scanListener;
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup viewGroup, Bundle bundle) {
this.container = ViewUtil.inflate(inflater, viewGroup, R.layout.verify_scan_fragment);
this.cameraView = container.findViewById(R.id.scanner);
this.cameraScrim = container.findViewById(R.id.camera_scrim);
this.cameraMarks = container.findViewById(R.id.camera_marks);
OneShotPreDrawListener.add(cameraScrim, () -> {
int width = cameraScrim.getScrimWidth();
int height = cameraScrim.getScrimHeight();
ViewUtil.updateLayoutParams(cameraMarks, width, height);
});
return container;
}
@Override
public void onResume() {
super.onResume();
this.scanningThread = new ScanningThread();
this.scanningThread.setScanListener(scanListener);
this.scanningThread.setCharacterSet("ISO-8859-1");
this.cameraView.onResume();
this.cameraView.setPreviewCallback(scanningThread);
this.scanningThread.start();
}
@Override
public void onPause() {
super.onPause();
this.cameraView.onPause();
this.scanningThread.stopScanning();
}
@Override
public void onConfigurationChanged(Configuration newConfiguration) {
super.onConfigurationChanged(newConfiguration);
this.cameraView.onPause();
this.cameraView.onResume();
this.cameraView.setPreviewCallback(scanningThread);
}
public void setScanListener(ScanListener listener) {
if (this.scanningThread != null) scanningThread.setScanListener(listener);
this.scanListener = listener;
}
}
}

View File

@@ -17,6 +17,8 @@
package org.thoughtcrime.securesms;
import static org.thoughtcrime.securesms.components.sensors.Orientation.PORTRAIT_BOTTOM_EDGE;
import android.Manifest;
import android.annotation.SuppressLint;
import android.app.PictureInPictureParams;
@@ -39,19 +41,16 @@ import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatDelegate;
import androidx.core.content.ContextCompat;
import androidx.core.util.Consumer;
import androidx.lifecycle.ViewModelProvider;
import androidx.lifecycle.ViewModelProviders;
import androidx.window.DisplayFeature;
import androidx.window.FoldingFeature;
import androidx.window.WindowLayoutInfo;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.core.util.logging.Log;
import org.signal.libsignal.protocol.IdentityKey;
import org.thoughtcrime.securesms.components.TooltipPopup;
import org.thoughtcrime.securesms.components.sensors.DeviceOrientationMonitor;
import org.thoughtcrime.securesms.components.webrtc.CallParticipantsListUpdatePopupWindow;
@@ -70,7 +69,6 @@ import org.thoughtcrime.securesms.messagerequests.CalleeMustAcceptMessageRequest
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.safety.SafetyNumberBottomSheet;
import org.thoughtcrime.securesms.service.webrtc.SignalCallManager;
import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.util.EllapsedTimeFormatter;
@@ -79,27 +77,20 @@ import org.thoughtcrime.securesms.util.FullscreenHelper;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.ThrottledDebouncer;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.VibrateUtil;
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
import org.thoughtcrime.securesms.webrtc.CallParticipantsViewState;
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.signalservice.api.messages.calls.HangupMessage;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.disposables.Disposable;
import static org.thoughtcrime.securesms.components.sensors.Orientation.PORTRAIT_BOTTOM_EDGE;
public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChangeDialog.Callback {
private static final String TAG = Log.tag(WebRtcCallActivity.class);
private static final int STANDARD_DELAY_FINISH = 1000;
private static final int VIBRATE_DURATION = 50;
public static final String ANSWER_ACTION = WebRtcCallActivity.class.getCanonicalName() + ".ANSWER_ACTION";
public static final String DENY_ACTION = WebRtcCallActivity.class.getCanonicalName() + ".DENY_ACTION";
@@ -115,13 +106,10 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
private TooltipPopup videoTooltip;
private WebRtcCallViewModel viewModel;
private boolean enableVideoIfAvailable;
private boolean hasWarnedAboutBluetooth;
private androidx.window.WindowManager windowManager;
private WindowLayoutInfoConsumer windowLayoutInfoConsumer;
private ThrottledDebouncer requestNewSizesThrottle;
private Disposable ephemeralStateDisposable = Disposable.empty();
@Override
protected void attachBaseContext(@NonNull Context newBase) {
getDelegate().setLocalNightMode(AppCompatDelegate.MODE_NIGHT_YES);
@@ -164,16 +152,6 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
requestNewSizesThrottle = new ThrottledDebouncer(TimeUnit.SECONDS.toMillis(1));
}
@Override
protected void onStart() {
super.onStart();
ephemeralStateDisposable = ApplicationDependencies.getSignalCallManager()
.ephemeralStates()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(viewModel::updateFromEphemeralState);
}
@Override
public void onResume() {
Log.i(TAG, "onResume()");
@@ -214,8 +192,6 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
Log.i(TAG, "onStop");
super.onStop();
ephemeralStateDisposable.dispose();
if (!isInPipMode() || isFinishing()) {
EventBus.getDefault().unregister(this);
requestNewSizesThrottle.clear();
@@ -279,6 +255,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
private void processIntent(@NonNull Intent intent) {
if (ANSWER_ACTION.equals(intent.getAction())) {
viewModel.setRecipient(EventBus.getDefault().getStickyEvent(WebRtcViewModel.class).getRecipient());
handleAnswerWithAudio();
} else if (DENY_ACTION.equals(intent.getAction())) {
handleDenyCall();
@@ -308,7 +285,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
WebRtcCallViewModel.Factory factory = new WebRtcCallViewModel.Factory(deviceOrientationMonitor);
viewModel = new ViewModelProvider(this, factory).get(WebRtcCallViewModel.class);
viewModel = ViewModelProviders.of(this, factory).get(WebRtcCallViewModel.class);
viewModel.setIsLandscapeEnabled(isLandscapeEnabled);
viewModel.setIsInPipMode(isInPipMode());
viewModel.getMicrophoneEnabled().observe(this, callScreen::setMicEnabled);
@@ -318,8 +295,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
LiveDataUtil.combineLatest(viewModel.getCallParticipantsState(),
viewModel.getOrientationAndLandscapeEnabled(),
viewModel.getEphemeralState(),
(s, o, e) -> new CallParticipantsViewState(s, e, o.first == PORTRAIT_BOTTOM_EDGE, o.second))
(s, o) -> new CallParticipantsViewState(s, o.first == PORTRAIT_BOTTOM_EDGE, o.second))
.observe(this, p -> callScreen.updateCallParticipants(p));
viewModel.getCallParticipantListUpdate().observe(this, participantUpdateWindow::addCallParticipantListUpdate);
viewModel.getSafetyNumberChangeEvent().observe(this, this::handleSafetyNumberChangeEvent);
@@ -345,8 +321,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
startCall(((WebRtcCallViewModel.Event.StartCall) event).isVideoCall());
return;
} else if (event instanceof WebRtcCallViewModel.Event.ShowGroupCallSafetyNumberChange) {
SafetyNumberBottomSheet.forGroupCall(((WebRtcCallViewModel.Event.ShowGroupCallSafetyNumberChange) event).getIdentityRecords())
.show(getSupportFragmentManager());
SafetyNumberChangeDialog.showForGroupCall(getSupportFragmentManager(), ((WebRtcCallViewModel.Event.ShowGroupCallSafetyNumberChange) event).getIdentityRecords());
return;
} else if (event instanceof WebRtcCallViewModel.Event.SwitchToSpeaker) {
callScreen.switchToSpeakerView();
@@ -391,15 +366,15 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
}
private void handleSetAudioHandset() {
ApplicationDependencies.getSignalCallManager().selectAudioDevice(SignalAudioManager.AudioDevice.EARPIECE);
ApplicationDependencies.getSignalCallManager().setAudioSpeaker(false);
}
private void handleSetAudioSpeaker() {
ApplicationDependencies.getSignalCallManager().selectAudioDevice(SignalAudioManager.AudioDevice.SPEAKER_PHONE);
ApplicationDependencies.getSignalCallManager().setAudioSpeaker(true);
}
private void handleSetAudioBluetooth() {
ApplicationDependencies.getSignalCallManager().selectAudioDevice(SignalAudioManager.AudioDevice.BLUETOOTH);
ApplicationDependencies.getSignalCallManager().setAudioBluetooth(true);
}
private void handleSetMuteAudio(boolean enabled) {
@@ -427,19 +402,24 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
}
private void handleAnswerWithAudio() {
Permissions.with(this)
.request(Manifest.permission.RECORD_AUDIO)
.ifNecessary()
.withRationaleDialog(getString(R.string.WebRtcCallActivity_to_answer_the_call_give_signal_access_to_your_microphone),
R.drawable.ic_mic_solid_24)
.withPermanentDenialDialog(getString(R.string.WebRtcCallActivity_signal_requires_microphone_and_camera_permissions_in_order_to_make_or_receive_calls))
.onAllGranted(() -> {
callScreen.setStatus(getString(R.string.RedPhone_answering));
Recipient recipient = viewModel.getRecipient().get();
ApplicationDependencies.getSignalCallManager().acceptCall(false);
})
.onAnyDenied(this::handleDenyCall)
.execute();
if (!recipient.equals(Recipient.UNKNOWN)) {
Permissions.with(this)
.request(Manifest.permission.RECORD_AUDIO)
.ifNecessary()
.withRationaleDialog(getString(R.string.WebRtcCallActivity_to_answer_the_call_from_s_give_signal_access_to_your_microphone, recipient.getDisplayName(this)),
R.drawable.ic_mic_solid_24)
.withPermanentDenialDialog(getString(R.string.WebRtcCallActivity_signal_requires_microphone_and_camera_permissions_in_order_to_make_or_receive_calls))
.onAllGranted(() -> {
callScreen.setRecipient(recipient);
callScreen.setStatus(getString(R.string.RedPhone_answering));
ApplicationDependencies.getSignalCallManager().acceptCall(false);
})
.onAnyDenied(this::handleDenyCall)
.execute();
}
}
private void handleAnswerWithVideo() {
@@ -503,12 +483,6 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
delayedFinish();
}
private void handleGlare(@NonNull Recipient recipient) {
Log.i(TAG, "handleGlare: " + recipient.getId());
callScreen.setStatus("");
}
private void handleCallRinging() {
callScreen.setStatus(getString(R.string.RedPhone_ringing));
}
@@ -526,11 +500,6 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
}
}
private void handleCallReconnecting() {
callScreen.setStatus(getString(R.string.WebRtcCallActivity__reconnecting));
VibrateUtil.vibrate(this, VIBRATE_DURATION);
}
private void handleRecipientUnavailable() {
EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class);
callScreen.setStatus(getString(R.string.RedPhone_recipient_unavailable));
@@ -563,7 +532,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
handleTerminate(recipient, HangupMessage.Type.NORMAL);
}
SafetyNumberBottomSheet.forCall(recipient.getId()).show(getSupportFragmentManager());
SafetyNumberChangeDialog.showForCall(getSupportFragmentManager(), recipient.getId());
}
public void handleSafetyNumberChangeEvent(@NonNull WebRtcCallViewModel.SafetyNumberChangeEvent safetyNumberChangeEvent) {
@@ -572,7 +541,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
GroupCallSafetyNumberChangeNotificationUtil.showNotification(this, viewModel.getRecipient().get());
} else {
GroupCallSafetyNumberChangeNotificationUtil.cancelNotification(this, viewModel.getRecipient().get());
SafetyNumberBottomSheet.forDuringGroupCall(safetyNumberChangeEvent.getRecipientIds()).show(getSupportFragmentManager());
SafetyNumberChangeDialog.showForDuringGroupCall(getSupportFragmentManager(), safetyNumberChangeEvent.getRecipientIds());
}
}
}
@@ -653,16 +622,12 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
handleCallPreJoin(event); break;
case CALL_CONNECTED:
handleCallConnected(event); break;
case CALL_RECONNECTING:
handleCallReconnecting(); break;
case NETWORK_FAILURE:
handleServerFailure(); break;
case CALL_RINGING:
handleCallRinging(); break;
case CALL_DISCONNECTED:
handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL); break;
case CALL_DISCONNECTED_GLARE:
handleGlare(event.getRecipient()); break;
case CALL_ACCEPTED_ELSEWHERE:
handleTerminate(event.getRecipient(), HangupMessage.Type.ACCEPTED); break;
case CALL_DECLINED_ELSEWHERE:
@@ -691,17 +656,6 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
enableVideoIfAvailable = false;
handleSetMuteVideo(false);
}
if (event.getBluetoothPermissionDenied() && !hasWarnedAboutBluetooth && !isFinishing()) {
new MaterialAlertDialogBuilder(this)
.setTitle(R.string.WebRtcCallActivity__bluetooth_permission_denied)
.setMessage(R.string.WebRtcCallActivity__please_enable_the_nearby_devices_permission_to_use_bluetooth_during_a_call)
.setPositiveButton(R.string.WebRtcCallActivity__open_settings, (d, w) -> startActivity(Permissions.getApplicationSettingsIntent(this)))
.setNegativeButton(R.string.WebRtcCallActivity__not_now, null)
.show();
hasWarnedAboutBluetooth = true;
}
}
private void handleCallPreJoin(@NonNull WebRtcViewModel event) {
@@ -850,7 +804,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
if (feature.isPresent()) {
FoldingFeature foldingFeature = (FoldingFeature) feature.get();
Rect bounds = foldingFeature.getBounds();
if (foldingFeature.isSeparating()) {
if (foldingFeature.getState() == FoldingFeature.State.HALF_OPENED && bounds.top == bounds.bottom) {
Log.d(TAG, "OnWindowLayoutInfo accepted: ensure call view is in table-top display mode");
viewModel.setFoldableState(WebRtcControls.FoldableState.folded(bounds.top));
} else {

View File

@@ -14,6 +14,7 @@ import android.view.animation.AccelerateInterpolator
import android.view.animation.DecelerateInterpolator
import android.view.animation.Interpolator
import androidx.annotation.RequiresApi
import org.thoughtcrime.securesms.components.AvatarImageView
private const val POSITION_ON_SCREEN = "signal.circleavatartransition.positiononscreen"
private const val WIDTH = "signal.circleavatartransition.width"
@@ -35,7 +36,7 @@ class CircleAvatarTransition(context: Context, attrs: AttributeSet?) : Transitio
private fun captureValues(transitionValues: TransitionValues) {
val view: View = transitionValues.view
if (view.transitionName == "avatar") {
if (view is AvatarImageView) {
val topLeft = intArrayOf(0, 0)
view.getLocationOnScreen(topLeft)
transitionValues.values[POSITION_ON_SCREEN] = topLeft
@@ -50,7 +51,7 @@ class CircleAvatarTransition(context: Context, attrs: AttributeSet?) : Transitio
}
val view: View = endValues.view
if (view.transitionName != "avatar") {
if (view !is AvatarImageView || view.transitionName != "avatar") {
return null
}

View File

@@ -1,63 +0,0 @@
package org.thoughtcrime.securesms.animation.transitions
import android.animation.Animator
import android.animation.ValueAnimator
import android.content.Context
import android.transition.Transition
import android.transition.TransitionValues
import android.util.AttributeSet
import android.view.ViewGroup
import androidx.annotation.RequiresApi
import androidx.core.animation.doOnEnd
import androidx.core.animation.doOnStart
@RequiresApi(21)
class CrossfaderTransition(context: Context, attrs: AttributeSet?) : Transition(context, attrs) {
companion object {
private const val WIDTH = "CrossfaderTransition.WIDTH"
}
override fun captureStartValues(transitionValues: TransitionValues) {
if (transitionValues.view is Crossfadeable) {
transitionValues.values[WIDTH] = transitionValues.view.width
}
}
override fun captureEndValues(transitionValues: TransitionValues) {
if (transitionValues.view is Crossfadeable) {
transitionValues.values[WIDTH] = transitionValues.view.width
}
}
override fun createAnimator(sceneRoot: ViewGroup?, startValues: TransitionValues?, endValues: TransitionValues?): Animator? {
if (startValues == null || endValues == null) {
return null
}
val startWidth = (startValues.values[WIDTH] ?: 0) as Int
val endWidth = (endValues.values[WIDTH] ?: 0) as Int
val view: Crossfadeable = endValues.view as? Crossfadeable ?: return null
val reverse = startWidth > endWidth
return ValueAnimator.ofFloat(0f, 1f).apply {
addUpdateListener {
view.onCrossfadeAnimationUpdated(it.animatedValue as Float, reverse)
}
doOnStart {
view.onCrossfadeStarted(reverse)
}
doOnEnd {
view.onCrossfadeFinished(reverse)
}
}
}
interface Crossfadeable {
fun onCrossfadeAnimationUpdated(progress: Float, reverse: Boolean)
fun onCrossfadeStarted(reverse: Boolean)
fun onCrossfadeFinished(reverse: Boolean)
}
}

View File

@@ -1,53 +0,0 @@
package org.thoughtcrime.securesms.animation.transitions
import android.animation.Animator
import android.animation.ValueAnimator
import android.content.Context
import android.transition.Transition
import android.transition.TransitionValues
import android.util.AttributeSet
import android.view.ViewGroup
import androidx.annotation.RequiresApi
import com.google.android.material.floatingactionbutton.FloatingActionButton
@RequiresApi(21)
class FabElevationFadeTransform(context: Context, attrs: AttributeSet?) : Transition(context, attrs) {
companion object {
private const val ELEVATION = "CrossfaderTransition.ELEVATION"
}
override fun captureStartValues(transitionValues: TransitionValues) {
if (transitionValues.view is FloatingActionButton) {
transitionValues.values[ELEVATION] = transitionValues.view.elevation
}
}
override fun captureEndValues(transitionValues: TransitionValues) {
if (transitionValues.view is FloatingActionButton) {
transitionValues.values[ELEVATION] = transitionValues.view.elevation
}
}
override fun createAnimator(sceneRoot: ViewGroup?, startValues: TransitionValues?, endValues: TransitionValues?): Animator? {
if (startValues?.view !is FloatingActionButton || endValues?.view !is FloatingActionButton) {
return null
}
val startElevation = startValues.view.elevation
val endElevation = endValues.view.elevation
if (startElevation == endElevation) {
return null
}
return ValueAnimator.ofFloat(
startValues.values[ELEVATION] as Float,
endValues.values[ELEVATION] as Float
).apply {
addUpdateListener {
val elevation = it.animatedValue as Float
endValues.view.elevation = elevation
}
}
}
}

View File

@@ -9,12 +9,12 @@ import org.thoughtcrime.securesms.blurhash.BlurHash;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.stickers.StickerLocator;
import org.thoughtcrime.securesms.util.Base64;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
public class PointerAttachment extends Attachment {
@@ -93,7 +93,7 @@ public class PointerAttachment extends Attachment {
}
public static Optional<Attachment> forPointer(Optional<SignalServiceAttachment> pointer, @Nullable StickerLocator stickerLocator, @Nullable String fastPreflightId) {
if (!pointer.isPresent() || !pointer.get().isPointer()) return Optional.empty();
if (!pointer.isPresent() || !pointer.get().isPointer()) return Optional.absent();
String encodedKey = null;
@@ -103,12 +103,12 @@ public class PointerAttachment extends Attachment {
return Optional.of(new PointerAttachment(pointer.get().getContentType(),
AttachmentDatabase.TRANSFER_PROGRESS_PENDING,
pointer.get().asPointer().getSize().orElse(0),
pointer.get().asPointer().getFileName().orElse(null),
pointer.get().asPointer().getSize().or(0),
pointer.get().asPointer().getFileName().orNull(),
pointer.get().asPointer().getCdnNumber(),
pointer.get().asPointer().getRemoteId().toString(),
encodedKey, null,
pointer.get().asPointer().getDigest().orElse(null),
pointer.get().asPointer().getDigest().orNull(),
fastPreflightId,
pointer.get().asPointer().getVoiceNote(),
pointer.get().asPointer().isBorderless(),
@@ -116,9 +116,9 @@ public class PointerAttachment extends Attachment {
pointer.get().asPointer().getWidth(),
pointer.get().asPointer().getHeight(),
pointer.get().asPointer().getUploadTimestamp(),
pointer.get().asPointer().getCaption().orElse(null),
pointer.get().asPointer().getCaption().orNull(),
stickerLocator,
BlurHash.parseOrNull(pointer.get().asPointer().getBlurHash().orElse(null))));
BlurHash.parseOrNull(pointer.get().asPointer().getBlurHash().orNull())));
}
@@ -127,13 +127,13 @@ public class PointerAttachment extends Attachment {
return Optional.of(new PointerAttachment(pointer.getContentType(),
AttachmentDatabase.TRANSFER_PROGRESS_PENDING,
thumbnail != null ? thumbnail.asPointer().getSize().orElse(0) : 0,
thumbnail != null ? thumbnail.asPointer().getSize().or(0) : 0,
pointer.getFileName(),
thumbnail != null ? thumbnail.asPointer().getCdnNumber() : 0,
thumbnail != null ? thumbnail.asPointer().getRemoteId().toString() : "0",
thumbnail != null && thumbnail.asPointer().getKey() != null ? Base64.encodeBytes(thumbnail.asPointer().getKey()) : null,
null,
thumbnail != null ? thumbnail.asPointer().getDigest().orElse(null) : null,
thumbnail != null ? thumbnail.asPointer().getDigest().orNull() : null,
null,
false,
false,
@@ -141,7 +141,7 @@ public class PointerAttachment extends Attachment {
thumbnail != null ? thumbnail.asPointer().getWidth() : 0,
thumbnail != null ? thumbnail.asPointer().getHeight() : 0,
thumbnail != null ? thumbnail.asPointer().getUploadTimestamp() : 0,
thumbnail != null ? thumbnail.asPointer().getCaption().orElse(null) : null,
thumbnail != null ? thumbnail.asPointer().getCaption().orNull() : null,
null,
null));
}

View File

@@ -1,12 +1,13 @@
package org.thoughtcrime.securesms.audio;
import android.annotation.TargetApi;
import android.media.AudioFormat;
import android.media.AudioRecord;
import android.media.MediaCodec;
import android.media.MediaCodecInfo;
import android.media.MediaFormat;
import android.media.MediaRecorder;
import android.os.ParcelFileDescriptor;
import android.os.Build;
import org.signal.core.util.StreamUtil;
import org.signal.core.util.logging.Log;
@@ -16,7 +17,8 @@ import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
public class AudioCodec implements Recorder {
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
public class AudioCodec {
private static final String TAG = Log.tag(AudioCodec.class);
@@ -49,19 +51,12 @@ public class AudioCodec implements Recorder {
}
}
@Override
public void start(ParcelFileDescriptor fileDescriptor) {
Log.i(TAG, "Recording voice note using AudioCodec.");
start(new ParcelFileDescriptor.AutoCloseOutputStream(fileDescriptor));
}
@Override
public synchronized void stop() {
running = false;
while (!finished) Util.wait(this, 0);
}
private void start(final OutputStream outputStream) {
public void start(final OutputStream outputStream) {
new Thread(new Runnable() {
@Override
public void run() {

View File

@@ -1,5 +1,6 @@
package org.thoughtcrime.securesms.audio;
import android.annotation.TargetApi;
import android.content.Context;
import android.net.Uri;
import android.os.Build;
@@ -19,6 +20,7 @@ import org.thoughtcrime.securesms.util.concurrent.SettableFuture;
import java.io.IOException;
import java.util.concurrent.ExecutorService;
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
public class AudioRecorder {
private static final String TAG = Log.tag(AudioRecorder.class);
@@ -27,8 +29,8 @@ public class AudioRecorder {
private final Context context;
private Recorder recorder;
private Uri captureUri;
private AudioCodec audioCodec;
private Uri captureUri;
public AudioRecorder(@NonNull Context context) {
this.context = context;
@@ -40,7 +42,7 @@ public class AudioRecorder {
executor.execute(() -> {
Log.i(TAG, "Running startRecording() + " + Thread.currentThread().getId());
try {
if (recorder != null) {
if (audioCodec != null) {
throw new AssertionError("We can only record once at a time.");
}
@@ -50,9 +52,9 @@ public class AudioRecorder {
.forData(new ParcelFileDescriptor.AutoCloseInputStream(fds[0]), 0)
.withMimeType(MediaUtil.AUDIO_AAC)
.createForDraftAttachmentAsync(context, () -> Log.i(TAG, "Write successful."), e -> Log.w(TAG, "Error during recording", e));
audioCodec = new AudioCodec();
recorder = Build.VERSION.SDK_INT >= 26 ? new MediaRecorderWrapper() : new AudioCodec();
recorder.start(fds[1]);
audioCodec.start(new ParcelFileDescriptor.AutoCloseOutputStream(fds[1]));
} catch (IOException e) {
Log.w(TAG, e);
}
@@ -65,12 +67,12 @@ public class AudioRecorder {
final SettableFuture<VoiceNoteDraft> future = new SettableFuture<>();
executor.execute(() -> {
if (recorder == null) {
if (audioCodec == null) {
sendToFuture(future, new IOException("MediaRecorder was never initialized successfully!"));
return;
}
recorder.stop();
audioCodec.stop();
try {
long size = MediaUtil.getMediaSize(context, captureUri);
@@ -80,7 +82,7 @@ public class AudioRecorder {
sendToFuture(future, ioe);
}
recorder = null;
audioCodec = null;
captureUri = null;
});

View File

@@ -22,7 +22,7 @@ import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.model.databaseprotos.AudioWaveFormData;
import org.thoughtcrime.securesms.media.DecryptableUriMediaInput;
import org.thoughtcrime.securesms.media.MediaInput;
@@ -100,7 +100,7 @@ public final class AudioWaveForm {
if (attachment instanceof DatabaseAttachment) {
try {
AttachmentDatabase attachmentDatabase = SignalDatabase.attachments();
AttachmentDatabase attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context);
DatabaseAttachment dbAttachment = (DatabaseAttachment) attachment;
long startTime = System.currentTimeMillis();

View File

@@ -1,65 +0,0 @@
package org.thoughtcrime.securesms.audio;
import android.media.MediaRecorder;
import android.os.ParcelFileDescriptor;
import org.signal.core.util.logging.Log;
import java.io.IOException;
/**
* Wrap Android's {@link MediaRecorder} for use with voice notes.
*/
public class MediaRecorderWrapper implements Recorder {
private static final String TAG = Log.tag(MediaRecorderWrapper.class);
private static final int SAMPLE_RATE = 44100;
private static final int CHANNELS = 1;
private static final int BIT_RATE = 32000;
private MediaRecorder recorder = null;
@Override
public void start(ParcelFileDescriptor fileDescriptor) throws IOException {
Log.i(TAG, "Recording voice note using MediaRecorderWrapper.");
recorder = new MediaRecorder();
try {
recorder.setAudioSource(MediaRecorder.AudioSource.MIC);
recorder.setOutputFormat(MediaRecorder.OutputFormat.AAC_ADTS);
recorder.setOutputFile(fileDescriptor.getFileDescriptor());
recorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
recorder.setAudioSamplingRate(SAMPLE_RATE);
recorder.setAudioEncodingBitRate(BIT_RATE);
recorder.setAudioChannels(CHANNELS);
recorder.prepare();
recorder.start();
} catch (IllegalStateException e) {
Log.w(TAG, "Unable to start recording", e);
recorder.release();
recorder = null;
throw new IOException(e);
}
}
@Override
public void stop() {
if (recorder == null) {
return;
}
try {
recorder.stop();
} catch (RuntimeException e) {
if (e.getClass() != RuntimeException.class) {
throw e;
} else {
Log.d(TAG, "Recording stopped with no data captured.");
}
} finally {
recorder.release();
recorder = null;
}
}
}

View File

@@ -1,13 +0,0 @@
package org.thoughtcrime.securesms.audio;
import android.os.ParcelFileDescriptor;
import java.io.IOException;
/**
* Simple abstraction of the interface for the original voice note recording and the new.
*/
public interface Recorder {
void start(ParcelFileDescriptor fileDescriptor) throws IOException;
void stop();
}

View File

@@ -1,6 +1,7 @@
package org.thoughtcrime.securesms.avatar
import android.os.Bundle
import java.lang.IllegalStateException
/**
* Utility class which encapsulates reading and writing Avatar objects to and from Bundles.

View File

@@ -4,10 +4,9 @@ import android.view.View
import android.widget.ImageView
import com.airbnb.lottie.SimpleColorFilter
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel
import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder
import org.thoughtcrime.securesms.util.MappingAdapter
import org.thoughtcrime.securesms.util.MappingModel
import org.thoughtcrime.securesms.util.MappingViewHolder
typealias OnAvatarColorClickListener = (Avatars.ColorPair) -> Unit
@@ -21,7 +20,7 @@ data class AvatarColorItem(
companion object {
fun registerViewHolder(adapter: MappingAdapter, onAvatarColorClickListener: OnAvatarColorClickListener) {
adapter.registerFactory(Model::class.java, LayoutFactory({ ViewHolder(it, onAvatarColorClickListener) }, R.layout.avatar_color_item))
adapter.registerFactory(Model::class.java, MappingAdapter.LayoutFactory({ ViewHolder(it, onAvatarColorClickListener) }, R.layout.avatar_color_item))
}
}

View File

@@ -3,7 +3,7 @@ package org.thoughtcrime.securesms.avatar
import android.content.Context
import android.net.Uri
import android.webkit.MimeTypeMap
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.mediasend.Media
import org.thoughtcrime.securesms.mms.PartAuthority
import org.thoughtcrime.securesms.util.MediaUtil
@@ -33,7 +33,7 @@ object AvatarPickerStorage {
@JvmStatic
fun cleanOrphans(context: Context) {
val avatarFiles = FileStorage.getAllFiles(context, DIRECTORY, FILENAME_BASE)
val database = SignalDatabase.avatarPicker
val database = DatabaseFactory.getAvatarPickerDatabase(context)
val photoAvatars = database
.getAllAvatars()
.filterIsInstance<Avatar.Photo>()

View File

@@ -14,10 +14,10 @@ import org.thoughtcrime.securesms.mms.PartAuthority
import org.thoughtcrime.securesms.profiles.AvatarHelper
import org.thoughtcrime.securesms.providers.BlobProvider
import org.thoughtcrime.securesms.util.MediaUtil
import org.whispersystems.libsignal.util.guava.Optional
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.IOException
import java.util.Optional
import javax.annotation.meta.Exhaustive
/**
@@ -128,6 +128,6 @@ object AvatarRenderer {
}
private fun createMedia(uri: Uri, size: Long): Media {
return Media(uri, MediaUtil.IMAGE_JPEG, System.currentTimeMillis(), DIMENSIONS, DIMENSIONS, size, 0, false, false, Optional.empty(), Optional.empty(), Optional.empty())
return Media(uri, MediaUtil.IMAGE_JPEG, System.currentTimeMillis(), DIMENSIONS, DIMENSIONS, size, 0, false, false, Optional.absent(), Optional.absent(), Optional.absent())
}
}

View File

@@ -143,10 +143,11 @@ object Avatars {
)
data class ColorPair(
@ColorInt val backgroundColor: Int,
@ColorInt val foregroundColor: Int,
val code: String
val backgroundAvatarColor: AvatarColor,
val foregroundAvatarColor: ForegroundColor
) {
constructor(backgroundAvatarColor: AvatarColor, foregroundAvatarColor: ForegroundColor) : this(backgroundAvatarColor.colorInt(), foregroundAvatarColor.colorInt, backgroundAvatarColor.serialize())
@ColorInt val backgroundColor: Int = backgroundAvatarColor.colorInt()
@ColorInt val foregroundColor: Int = foregroundAvatarColor.colorInt
val code: String = backgroundAvatarColor.serialize()
}
}

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