Compare commits

..

2 Commits

Author SHA1 Message Date
Alex Hart
72a2ef275c Bump version to 5.29.7 2022-01-25 10:26:22 -04:00
Alex Hart
318f3589d6 Remove clear-cache call from database migration. 2022-01-25 10:25:27 -04:00
2635 changed files with 37174 additions and 142389 deletions

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

@@ -51,13 +51,6 @@
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="2147483647" />
<option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="2147483647" />
</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" />

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

@@ -7,9 +7,14 @@ apply from: 'translations.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 {
@@ -57,31 +62,33 @@ ktlint {
version = "0.43.2"
}
def canonicalVersionCode = 1082
def canonicalVersionName = "5.42.4"
def canonicalVersionCode = 990
def canonicalVersionName = "5.29.7"
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',
'nightlyProdFlipper',
'nightlyProdPerf',
'nightlyProdRelease',
'playProdDebug',
'playProdSpinner',
'playProdFlipper',
'playProdPerf',
'playProdRelease',
'playStagingDebug',
'playStagingSpinner',
'playStagingFlipper',
'playStagingPerf',
'playStagingRelease',
'websiteProdSpinner',
'studyProdMock',
'studyProdPerf',
'websiteProdFlipper',
'websiteProdRelease',
]
@@ -112,50 +119,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
@@ -175,7 +138,7 @@ android {
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_CDSH_URL", "\"https://cdsh.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,28 +146,21 @@ 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", "CDSH_PUBLIC_KEY", "\"2fe57da347cd62431528daac5fbb290730fff684afc4cfc2ed90995f58cb3b74\""
buildConfigField "String", "CDSH_CODE_HASH", "\"ec58c0d7561de8d5657f3a4b22a635eaa305204e9359dcc80a99dfd0c5f1cbf2\""
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_ENCLAVE", "new org.thoughtcrime.securesms.KbsEnclave(\"fe7c1bfae98f9b073d220366ea31163ee82f6d04bead774f71ca8e5c40847bfe\"," +
"\"fe7c1bfae98f9b073d220366ea31163ee82f6d04bead774f71ca8e5c40847bfe\", " +
"\"a3baab19ef6ce6f34ab9ebb25ba722725ae44a8872dc0ff08ad6d83a9489de87\")";
buildConfigField "org.thoughtcrime.securesms.KbsEnclave[]", "KBS_FALLBACKS", "new org.thoughtcrime.securesms.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[]", "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_BLACKLIST", "new int[]{98,963,53,850,7}"
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\""
@@ -232,13 +188,45 @@ android {
testInstrumentationRunnerArguments clearPackageData: 'true'
}
testOptions {
execution 'ANDROIDX_TEST_ORCHESTRATOR'
}
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 '/org/spongycastle/x509/CertPathReviewerMessages.properties'
exclude '/org/spongycastle/x509/CertPathReviewerMessages_de.properties'
}
buildTypes {
debug {
if (keystores['debug'] != null) {
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 +235,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 +251,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 +267,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 {
@@ -311,6 +306,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'
@@ -332,14 +337,13 @@ android {
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_ENCLAVE", "new org.thoughtcrime.securesms.KbsEnclave(\"823a3b2c037ff0cbe305cc48928cfcc97c9ed4a8ca6d49af6f7d6981fb60a4e9\", " +
"\"16b94ac6d2b7f7b9d72928f36d798dbb35ed32e7bb14c42b4301ad0344b46f29\", " +
"\"a3baab19ef6ce6f34ab9ebb25ba722725ae44a8872dc0ff08ad6d83a9489de87\")"
buildConfigField "org.thoughtcrime.securesms.KbsEnclave[]", "KBS_FALLBACKS", "new org.thoughtcrime.securesms.KbsEnclave[0]"
buildConfigField "String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx\""
buildConfigField "String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"ABSY21VckQcbSXVNCGRYJcfWHiAMZmpTtTELcDmxgdFbtp/bWsSxZdMKzfCp8rvIs8ocCU3B37fT3r4Mi5qAemeGeR2X+/YmOGR5ofui7tD5mDQfstAI9i+4WpMtIe8KC3wU5w3Inq3uNWVmoGtpKndsNfwJrCg0Hd9zmObhypUnSkfYn2ooMOOnBpfdanRtrvetZUayDMSC5iSRcXKpdlukrpzzsCIvEwjwQlJYVPOQPj4V0F4UXXBdHSLK05uoPBCQG8G9rYIGedYsClJXnbrgGYG3eMTG5hnx4X4ntARBgELuMWWUEEfSK0mjXg+/2lPmWcTZWR9nkqgQQP0tbzuiPm74H2wMO4u1Wafe+UwyIlIT9L7KLS19Aw8r4sPrXQ==\""
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\""
@@ -353,9 +357,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 +381,19 @@ android {
variant.setIgnore(true)
}
}
lintOptions {
checkReleaseBuilds false
abortOnError true
baseline file("lint-baseline.xml")
disable "LintError"
}
testOptions {
unitTests {
includeAndroidResources = true
}
}
}
dependencies {
@@ -442,10 +456,8 @@ dependencies {
implementation project(':device-transfer')
implementation project(':image-editor')
implementation project(':donations')
implementation project(':contacts')
implementation project(':qr')
implementation libs.libsignal.android
implementation libs.signal.client.android
implementation libs.google.protobuf.javalite
implementation(libs.mobilecoin) {
@@ -470,6 +482,7 @@ dependencies {
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.google.zxing.core
@@ -495,6 +508,7 @@ dependencies {
implementation libs.lottie
implementation libs.stickyheadergrid
implementation libs.circular.progress.button
implementation libs.signal.android.database.sqlcipher
implementation libs.androidx.sqlite
@@ -505,31 +519,29 @@ dependencies {
}
implementation libs.dnsjava
spinnerImplementation project(":spinner")
spinnerImplementation libs.square.leakcanary
flipperImplementation libs.facebook.flipper
flipperImplementation libs.facebook.soloader
flipperImplementation libs.square.leakcanary
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
@@ -541,7 +553,7 @@ dependencies {
implementation libs.rxjava3.rxkotlin
implementation libs.rxdogtag
androidTestUtil 'androidx.test:orchestrator:1.4.1'
androidTestUtil 'androidx.test:orchestrator:1.4.0'
}
def getLastCommitTimestamp() {

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,40 +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.conversation.ui.error.SafetyNumberChangeDialog
import org.thoughtcrime.securesms.database.IdentityDatabase
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.profiles.ProfileName
import org.thoughtcrime.securesms.recipients.Recipient
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()
@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 {
SafetyNumberChangeDialog.show(it.supportFragmentManager, other.id)
}
// 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,241 +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.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)
}
}

View File

@@ -1,67 +0,0 @@
package org.thoughtcrime.securesms.database
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,
giftBadge: GiftBadge? = null
): Long {
val message = OutgoingMediaMessage(
recipient,
body,
emptyList(),
sentTimeMillis,
subscriptionId,
expiresIn,
viewOnce,
distributionType,
storyType,
null,
false,
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

@@ -0,0 +1,436 @@
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.assertNotEquals
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
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.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.whispersystems.libsignal.util.guava.Optional
import org.whispersystems.signalservice.api.push.ACI
import java.lang.IllegalArgumentException
import java.util.UUID
@RunWith(AndroidJUnit4::class)
class RecipientDatabaseTest {
private lateinit var recipientDatabase: RecipientDatabase
@Before
fun setup() {
recipientDatabase = SignalDatabase.recipients
ensureDbEmpty()
}
// ==============================================================
// 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_highTrust() {
val recipientId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, null, true)
val recipient = Recipient.resolved(recipientId)
assertEquals(ACI_A, recipient.requireAci())
assertFalse(recipient.hasE164())
}
/** If all you have is an ACI, you can just store that, regardless of trust level. */
@Test
fun getAndPossiblyMerge_aciAndE164MapToNoOne_aciOnly_lowTrust() {
val recipientId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, null, false)
val recipient = Recipient.resolved(recipientId)
assertEquals(ACI_A, recipient.requireAci())
assertFalse(recipient.hasE164())
}
/** If all you have is an E164, you can just store that, regardless of trust level. */
@Test
fun getAndPossiblyMerge_aciAndE164MapToNoOne_e164Only_highTrust() {
val recipientId: RecipientId = recipientDatabase.getAndPossiblyMerge(null, E164_A, true)
val recipient = Recipient.resolved(recipientId)
assertEquals(E164_A, recipient.requireE164())
assertFalse(recipient.hasAci())
}
/** If all you have is an E164, you can just store that, regardless of trust level. */
@Test
fun getAndPossiblyMerge_aciAndE164MapToNoOne_e164Only_lowTrust() {
val recipientId: RecipientId = recipientDatabase.getAndPossiblyMerge(null, E164_A, false)
val recipient = Recipient.resolved(recipientId)
assertEquals(E164_A, recipient.requireE164())
assertFalse(recipient.hasAci())
}
/** With high trust, you can associate an ACI-e164 pair. */
@Test
fun getAndPossiblyMerge_aciAndE164MapToNoOne_aciAndE164_highTrust() {
val recipientId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
val recipient = Recipient.resolved(recipientId)
assertEquals(ACI_A, recipient.requireAci())
assertEquals(E164_A, recipient.requireE164())
}
/** With low trust, you cannot associate an ACI-e164 pair, and therefore can only store the ACI. */
@Test
fun getAndPossiblyMerge_aciAndE164MapToNoOne_aciAndE164_lowTrust() {
val recipientId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, false)
val recipient = Recipient.resolved(recipientId)
assertEquals(ACI_A, recipient.requireAci())
assertFalse(recipient.hasE164())
}
// ==============================================================
// If the ACI maps to an existing user, but the E164 doesn't
// ==============================================================
/** With high trust, you can associate an e164 with an existing ACI. */
@Test
fun getAndPossiblyMerge_aciMapsToExistingUserButE164DoesNot_aciAndE164_highTrust() {
val existingId: RecipientId = recipientDatabase.getOrInsertFromAci(ACI_A)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
assertEquals(existingId, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireAci())
assertEquals(E164_A, retrievedRecipient.requireE164())
}
/** With low trust, you cannot associate an ACI-e164 pair, and therefore cannot store the e164. */
@Test
fun getAndPossiblyMerge_aciMapsToExistingUserButE164DoesNot_aciAndE164_lowTrust() {
val existingId: RecipientId = recipientDatabase.getOrInsertFromAci(ACI_A)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, false)
assertEquals(existingId, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireAci())
assertFalse(retrievedRecipient.hasE164())
}
/** Basically the change number case. High trust lets you update the existing user. */
@Test
fun getAndPossiblyMerge_aciMapsToExistingUserButE164DoesNot_aciAndE164_2_highTrust() {
val existingId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_B, true)
assertEquals(existingId, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireAci())
assertEquals(E164_B, retrievedRecipient.requireE164())
}
/** Low trust means you cant update the underlying data, but you also dont need to create any new rows. */
@Test
fun getAndPossiblyMerge_aciMapsToExistingUserButE164DoesNot_aciAndE164_2_lowTrust() {
val existingId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_B, false)
assertEquals(existingId, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireAci())
assertEquals(E164_A, retrievedRecipient.requireE164())
}
// ==============================================================
// If the E164 maps to an existing user, but the ACI doesn't
// ==============================================================
/** With high trust, you can associate an e164 with an existing ACI. */
@Test
fun getAndPossiblyMerge_e164MapsToExistingUserButAciDoesNot_aciAndE164_highTrust() {
val existingId: RecipientId = recipientDatabase.getOrInsertFromE164(E164_A)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
assertEquals(existingId, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireAci())
assertEquals(E164_A, retrievedRecipient.requireE164())
}
/** With low trust, you cannot associate an ACI-e164 pair, and therefore need to create a new person with just the ACI. */
@Test
fun getAndPossiblyMerge_e164MapsToExistingUserButAciDoesNot_aciAndE164_lowTrust() {
val existingId: RecipientId = recipientDatabase.getOrInsertFromE164(E164_A)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, false)
assertNotEquals(existingId, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireAci())
assertFalse(retrievedRecipient.hasE164())
val existingRecipient = Recipient.resolved(existingId)
assertEquals(E164_A, existingRecipient.requireE164())
assertFalse(existingRecipient.hasAci())
}
/** We never change the ACI of an existing row. New ACI = new person, regardless of trust. But high trust lets us take the e164 from the current holder. */
@Test
fun getAndPossiblyMerge_e164MapsToExistingUserButAciDoesNot_aciAndE164_2_highTrust() {
val existingId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_B, E164_A, true)
assertNotEquals(existingId, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_B, retrievedRecipient.requireAci())
assertEquals(E164_A, retrievedRecipient.requireE164())
val existingRecipient = Recipient.resolved(existingId)
assertEquals(ACI_A, existingRecipient.requireAci())
assertFalse(existingRecipient.hasE164())
}
/** We never change the ACI of an existing row. New ACI = new person, regardless of trust. And low trust means we cant take the e164. */
@Test
fun getAndPossiblyMerge_e164MapsToExistingUserButAciDoesNot_aciAndE164_2_lowTrust() {
val existingId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_B, E164_A, false)
assertNotEquals(existingId, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_B, retrievedRecipient.requireAci())
assertFalse(retrievedRecipient.hasE164())
val existingRecipient = Recipient.resolved(existingId)
assertEquals(ACI_A, existingRecipient.requireAci())
assertEquals(E164_A, existingRecipient.requireE164())
}
/** We never want to remove the e164 of our own contact entry. So basically treat this as a low-trust case, and leave the e164 alone. */
@Test
fun getAndPossiblyMerge_e164MapsToExistingUserButAciDoesNot_e164BelongsToLocalUser_highTrust() {
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.getAndPossiblyMerge(ACI_A, E164_A, true)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_B, E164_A, true)
assertNotEquals(existingId, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_B, retrievedRecipient.requireAci())
assertFalse(retrievedRecipient.hasE164())
val existingRecipient = Recipient.resolved(existingId)
assertEquals(ACI_A, existingRecipient.requireAci())
assertEquals(E164_A, existingRecipient.requireE164())
}
// ==============================================================
// If both the ACI and E164 map to an existing user
// ==============================================================
/** Regardless of trust, if your ACI and e164 match, youre good. */
@Test
fun getAndPossiblyMerge_bothAciAndE164MapToExistingUser_aciAndE164_highTrust() {
val existingId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
assertEquals(existingId, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireAci())
assertEquals(E164_A, retrievedRecipient.requireE164())
}
/** 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_bothAciAndE164MapToExistingUser_aciAndE164_merge_highTrust() {
val existingAciId: RecipientId = recipientDatabase.getOrInsertFromAci(ACI_A)
val existingE164Id: RecipientId = recipientDatabase.getOrInsertFromE164(E164_A)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
assertEquals(existingAciId, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireAci())
assertEquals(E164_A, retrievedRecipient.requireE164())
val existingE164Recipient = Recipient.resolved(existingE164Id)
assertEquals(retrievedId, existingE164Recipient.id)
}
/** Low trust means you cant merge. If youre retrieving a user from the table with this data, prefer the ACI one. */
@Test
fun getAndPossiblyMerge_bothAciAndE164MapToExistingUser_aciAndE164_lowTrust() {
val existingAciId: RecipientId = recipientDatabase.getOrInsertFromAci(ACI_A)
val existingE164Id: RecipientId = recipientDatabase.getOrInsertFromE164(E164_A)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, false)
assertEquals(existingAciId, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireAci())
assertFalse(retrievedRecipient.hasE164())
val existingE164Recipient = Recipient.resolved(existingE164Id)
assertEquals(E164_A, existingE164Recipient.requireE164())
assertFalse(existingE164Recipient.hasAci())
}
/** Another high trust case. No new rules here, just a more complex scenario to show how different rules interact. */
@Test
fun getAndPossiblyMerge_bothAciAndE164MapToExistingUser_aciAndE164_complex_highTrust() {
val existingId1: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_B, true)
val existingId2: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_B, E164_A, true)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
assertEquals(existingId1, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireAci())
assertEquals(E164_A, retrievedRecipient.requireE164())
val existingRecipient2 = Recipient.resolved(existingId2)
assertEquals(ACI_B, existingRecipient2.requireAci())
assertFalse(existingRecipient2.hasE164())
}
/** Another low trust case. No new rules here, just a more complex scenario to show how different rules interact. */
@Test
fun getAndPossiblyMerge_bothAciAndE164MapToExistingUser_aciAndE164_complex_lowTrust() {
val existingId1: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_B, true)
val existingId2: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_B, E164_A, true)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, false)
assertEquals(existingId1, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireAci())
assertEquals(E164_B, retrievedRecipient.requireE164())
val existingRecipient2 = Recipient.resolved(existingId2)
assertEquals(ACI_B, existingRecipient2.requireAci())
assertEquals(E164_A, existingRecipient2.requireE164())
}
/**
* Another high trust 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_highTrust() {
val existingId1: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_B, true)
val existingId2: RecipientId = recipientDatabase.getAndPossiblyMerge(null, E164_A, true)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
assertEquals(existingId1, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireAci())
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. So basically treat this as a low-trust case, and 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.getAndPossiblyMerge(ACI_B, E164_A, true)
val existingId2: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, null, true)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
assertEquals(existingId2, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireAci())
assertFalse(retrievedRecipient.hasE164())
val recipientWithId1 = Recipient.resolved(existingId1)
assertEquals(ACI_B, recipientWithId1.requireAci())
assertEquals(E164_A, recipientWithId1.requireE164())
}
// ==============================================================
// 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.getOrInsertFromAci(ACI_A)
// WHEN I retrieve one by UUID
val possible: Optional<RecipientId> = recipientDatabase.getByAci(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.aci.isPresent)
assertEquals(ACI_A, recipient.aci.get())
}
@Test(expected = IllegalArgumentException::class)
fun getAndPossiblyMerge_noArgs_invalid() {
recipientDatabase.getAndPossiblyMerge(null, null, true)
}
private fun ensureDbEmpty() {
SignalDatabase.rawDatabase.rawQuery("SELECT COUNT(*) FROM ${RecipientDatabase.TABLE_NAME}", null).use { cursor ->
assertTrue(cursor.moveToFirst())
assertEquals(0, cursor.getLong(0))
}
}
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"))
const val E164_A = "+12221234567"
const val E164_B = "+13331234567"
}
}

View File

@@ -1,786 +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_getAndPossiblyMerge {
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_highTrust() {
val recipientId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, null, true)
val recipient = Recipient.resolved(recipientId)
assertEquals(ACI_A, recipient.requireServiceId())
assertFalse(recipient.hasE164())
}
/** If all you have is an ACI, you can just store that, regardless of trust level. */
@Test
fun getAndPossiblyMerge_aciAndE164MapToNoOne_aciOnly_lowTrust() {
val recipientId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, null, false)
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_highTrust() {
val recipientId: RecipientId = recipientDatabase.getAndPossiblyMerge(null, E164_A, true)
val recipient = Recipient.resolved(recipientId)
assertEquals(E164_A, recipient.requireE164())
assertFalse(recipient.hasServiceId())
}
/** If all you have is an E164, you can just store that, regardless of trust level. */
@Test
fun getAndPossiblyMerge_aciAndE164MapToNoOne_e164Only_lowTrust() {
val recipientId: RecipientId = recipientDatabase.getAndPossiblyMerge(null, E164_A, false)
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_highTrust() {
val recipientId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
val recipient = Recipient.resolved(recipientId)
assertEquals(ACI_A, recipient.requireServiceId())
assertEquals(E164_A, recipient.requireE164())
}
/** With low trust, you cannot associate an ACI-e164 pair, and therefore can only store the ACI. */
@Test
fun getAndPossiblyMerge_aciAndE164MapToNoOne_aciAndE164_lowTrust() {
val recipientId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, false)
val recipient = Recipient.resolved(recipientId)
assertEquals(ACI_A, recipient.requireServiceId())
assertFalse(recipient.hasE164())
}
// ==============================================================
// If the ACI maps to an existing user, but the E164 doesn't
// ==============================================================
/** With high trust, you can associate an e164 with an existing ACI. */
@Test
fun getAndPossiblyMerge_aciMapsToExistingUserButE164DoesNot_aciAndE164_highTrust() {
val existingId: RecipientId = recipientDatabase.getOrInsertFromServiceId(ACI_A)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
assertEquals(existingId, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
assertEquals(E164_A, retrievedRecipient.requireE164())
}
/** With low trust, you cannot associate an ACI-e164 pair, and therefore cannot store the e164. */
@Test
fun getAndPossiblyMerge_aciMapsToExistingUserButE164DoesNot_aciAndE164_lowTrust() {
val existingId: RecipientId = recipientDatabase.getOrInsertFromServiceId(ACI_A)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, false)
assertEquals(existingId, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
assertFalse(retrievedRecipient.hasE164())
}
/** Basically the change number case. High trust lets you update the existing user. */
@Test
fun getAndPossiblyMerge_aciMapsToExistingUserButE164DoesNot_aciAndE164_2_highTrust() {
val existingId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_B, true)
assertEquals(existingId, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
assertEquals(E164_B, retrievedRecipient.requireE164())
}
/** Low trust means you cant update the underlying data, but you also dont need to create any new rows. */
@Test
fun getAndPossiblyMerge_aciMapsToExistingUserButE164DoesNot_aciAndE164_2_lowTrust() {
val existingId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_B, false)
assertEquals(existingId, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
assertEquals(E164_A, retrievedRecipient.requireE164())
}
// ==============================================================
// If the E164 maps to an existing user, but the ACI doesn't
// ==============================================================
/** With high trust, you can associate an e164 with an existing ACI. */
@Test
fun getAndPossiblyMerge_e164MapsToExistingUserButAciDoesNot_aciAndE164_highTrust() {
val existingId: RecipientId = recipientDatabase.getOrInsertFromE164(E164_A)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
assertEquals(existingId, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
assertEquals(E164_A, retrievedRecipient.requireE164())
}
/** With low trust, you cannot associate an ACI-e164 pair, and therefore need to create a new person with just the ACI. */
@Test
fun getAndPossiblyMerge_e164MapsToExistingUserButAciDoesNot_aciAndE164_lowTrust() {
val existingId: RecipientId = recipientDatabase.getOrInsertFromE164(E164_A)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, false)
assertNotEquals(existingId, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
assertFalse(retrievedRecipient.hasE164())
val existingRecipient = Recipient.resolved(existingId)
assertEquals(E164_A, existingRecipient.requireE164())
assertFalse(existingRecipient.hasServiceId())
}
/** We never change the ACI of an existing row. New ACI = new person, regardless of trust. But high trust lets us take the e164 from the current holder. */
@Test
fun getAndPossiblyMerge_e164MapsToExistingUserButAciDoesNot_aciAndE164_2_highTrust() {
val existingId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
recipientDatabase.setPni(existingId, PNI_A)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_B, E164_A, true)
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 change the ACI of an existing row. New ACI = new person, regardless of trust. And low trust means we cant take the e164. */
@Test
fun getAndPossiblyMerge_e164MapsToExistingUserButAciDoesNot_aciAndE164_2_lowTrust() {
val existingId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_B, E164_A, false)
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())
}
/** We never want to remove the e164 of our own contact entry. So basically treat this as a low-trust case, and leave the e164 alone. */
@Test
fun getAndPossiblyMerge_e164MapsToExistingUserButAciDoesNot_e164BelongsToLocalUser_highTrust() {
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.getAndPossiblyMerge(ACI_A, E164_A, true)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_B, E164_A, true)
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
// ==============================================================
/** Regardless of trust, if your ACI and e164 match, youre good. */
@Test
fun getAndPossiblyMerge_bothAciAndE164MapToExistingUser_aciAndE164_highTrust() {
val existingId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
assertEquals(existingId, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
assertEquals(E164_A, retrievedRecipient.requireE164())
}
/** 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_bothAciAndE164MapToExistingUser_aciAndE164_merge_highTrust() {
val changeNumberListener = ChangeNumberListener()
changeNumberListener.enqueue()
val existingAciId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, null, true)
val existingE164Id: RecipientId = recipientDatabase.getAndPossiblyMerge(null, E164_A, true)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
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_highTrust], but with a number change. */
@Test
fun getAndPossiblyMerge_bothAciAndE164MapToExistingUser_aciAndE164_merge_highTrust_changedNumber() {
val changeNumberListener = ChangeNumberListener()
changeNumberListener.enqueue()
val existingAciId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_B, true)
val existingE164Id: RecipientId = recipientDatabase.getAndPossiblyMerge(null, E164_A, true)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
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)
}
/** Low trust means you cant merge. If youre retrieving a user from the table with this data, prefer the ACI one. */
@Test
fun getAndPossiblyMerge_bothAciAndE164MapToExistingUser_aciAndE164_lowTrust() {
val existingAciId: RecipientId = recipientDatabase.getOrInsertFromServiceId(ACI_A)
val existingE164Id: RecipientId = recipientDatabase.getOrInsertFromE164(E164_A)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, false)
assertEquals(existingAciId, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
assertFalse(retrievedRecipient.hasE164())
val existingE164Recipient = Recipient.resolved(existingE164Id)
assertEquals(E164_A, existingE164Recipient.requireE164())
assertFalse(existingE164Recipient.hasServiceId())
}
/** Another high trust case. No new rules here, just a more complex scenario to show how different rules interact. */
@Test
fun getAndPossiblyMerge_bothAciAndE164MapToExistingUser_aciAndE164_complex_highTrust() {
val changeNumberListener = ChangeNumberListener()
changeNumberListener.enqueue()
val existingId1: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_B, true)
val existingId2: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_B, E164_A, true)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
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 low trust case. No new rules here, just a more complex scenario to show how different rules interact. */
@Test
fun getAndPossiblyMerge_bothAciAndE164MapToExistingUser_aciAndE164_complex_lowTrust() {
val existingId1: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_B, true)
val existingId2: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_B, E164_A, true)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, false)
assertEquals(existingId1, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
assertEquals(E164_B, retrievedRecipient.requireE164())
val existingRecipient2 = Recipient.resolved(existingId2)
assertEquals(ACI_B, existingRecipient2.requireServiceId())
assertEquals(E164_A, existingRecipient2.requireE164())
}
/**
* Another high trust 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_highTrust() {
val existingId1: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_B, true)
val existingId2: RecipientId = recipientDatabase.getAndPossiblyMerge(null, E164_A, true)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
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. So basically treat this as a low-trust case, and 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.getAndPossiblyMerge(ACI_B, E164_A, true)
val existingId2: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, null, true)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
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_highTrust_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.getAndPossiblyMerge(ACI_A, E164_A, true)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_B, highTrust = true, 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_highTrust_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.getAndPossiblyMerge(ACI_A, E164_A, true)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_B, highTrust = true, 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_highTrust_changedNumber() {
val changeNumberListener = ChangeNumberListener()
changeNumberListener.enqueue()
val existingId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_B, true)
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.getAndPossiblyMerge(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.getAndPossiblyMerge(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

@@ -0,0 +1,262 @@
package org.thoughtcrime.securesms.database
import android.app.Application
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.signal.storageservice.protos.groups.local.DecryptedGroup
import org.signal.storageservice.protos.groups.local.DecryptedMember
import org.signal.zkgroup.groups.GroupMasterKey
import org.thoughtcrime.securesms.conversation.colors.AvatarColor
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.groups.GroupId
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.thoughtcrime.securesms.util.CursorUtil
import org.whispersystems.libsignal.IdentityKey
import org.whispersystems.libsignal.SignalProtocolAddress
import org.whispersystems.libsignal.state.SessionRecord
import org.whispersystems.libsignal.util.guava.Optional
import org.whispersystems.signalservice.api.push.ACI
import org.whispersystems.signalservice.api.util.UuidUtil
import java.util.UUID
@RunWith(AndroidJUnit4::class)
class RecipientDatabaseTest_merges {
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
@Before
fun setup() {
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
ensureDbEmpty()
}
/** 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_general() {
// Setup
val recipientIdAci: RecipientId = recipientDatabase.getOrInsertFromAci(ACI_A)
val recipientIdE164: RecipientId = recipientDatabase.getOrInsertFromE164(E164_A)
val recipientIdAciB: RecipientId = recipientDatabase.getOrInsertFromAci(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(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)
// Merge
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(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.requireAci())
assertEquals(E164_A, retrievedRecipient.requireE164())
val existingE164Recipient = Recipient.resolved(recipientIdE164)
assertEquals(retrievedId, existingE164Recipient.id)
// Thread validation
assertEquals(threadIdAci, retrievedThreadId)
assertNull(threadDatabase.getThreadIdFor(recipientIdE164))
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)
assertNull(identityDatabase.getIdentityStoreRecord(E164_A))
// Session validation
assertNotNull(sessionDatabase.load(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)!!
assertThat("Notification Profile 1 should now only contain ACI $recipientIdAci", updatedProfile1.allowedMembers, Matchers.containsInAnyOrder(recipientIdAci))
assertThat("Notification Profile 2 should now contain ACI A ($recipientIdAci) and ACI B ($recipientIdAciB)", updatedProfile2.allowedMembers, Matchers.containsInAnyOrder(recipientIdAci, recipientIdAciB))
}
private val context: Application
get() = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as Application
private fun ensureDbEmpty() {
SignalDatabase.rawDatabase.rawQuery("SELECT COUNT(*) FROM ${RecipientDatabase.TABLE_NAME}", 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.absent()): 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.absent()): IncomingMediaMessage {
return IncomingMediaMessage(sender, groupId, body, time, time, time, emptyList(), 0, 0, false, false, true, Optional.absent())
}
private fun identityKey(value: Byte): IdentityKey {
val bytes = ByteArray(33)
bytes[0] = 0x05
bytes[1] = value
return IdentityKey(bytes)
}
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)
)
}
}
private fun notificationProfile(name: String): NotificationProfile {
return (notificationProfileDatabase.createProfile(name = name, emoji = "", color = AvatarColor.A210, System.currentTimeMillis()) as NotificationProfileDatabase.NotificationProfileChangeResult.Success).notificationProfile
}
/** 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
)
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 E164_A = "+12221234567"
val E164_B = "+13331234567"
}
}

View File

@@ -1,206 +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.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.util.UUID
@RunWith(AndroidJUnit4::class)
class RecipientDatabaseTest_processCdsV2Result {
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 processCdsV2Result_noMatch() {
// Note that we haven't inserted any test data
val resultId: RecipientId = recipientDatabase.processCdsV2Result(E164_A, PNI_A, ACI_A)
val record: IdRecord = require(resultId)
assertEquals(resultId, record.id)
assertEquals(E164_A, record.e164)
assertEquals(ACI_A, record.sid)
assertEquals(PNI_A, record.pni)
}
@Test
fun processCdsV2Result_fullMatch() {
val inputId: RecipientId = insert(E164_A, PNI_A, ACI_A)
val resultId: RecipientId = recipientDatabase.processCdsV2Result(E164_A, PNI_A, ACI_A)
val record: IdRecord = require(resultId)
assertEquals(inputId, record.id)
assertEquals(E164_A, record.e164)
assertEquals(ACI_A, record.sid)
assertEquals(PNI_A, record.pni)
}
@Test
fun processCdsV2Result_onlyE164Matches() {
val inputId: RecipientId = insert(E164_A, null, null)
val resultId: RecipientId = recipientDatabase.processCdsV2Result(E164_A, PNI_A, ACI_A)
val record: IdRecord = require(resultId)
assertEquals(inputId, record.id)
assertEquals(E164_A, record.e164)
assertEquals(ACI_A, record.sid)
assertEquals(PNI_A, record.pni)
}
@Test
fun processCdsV2Result_e164AndPniMatches() {
val inputId: RecipientId = insert(E164_A, PNI_A, null)
val resultId: RecipientId = recipientDatabase.processCdsV2Result(E164_A, PNI_A, ACI_A)
val record: IdRecord = require(resultId)
assertEquals(inputId, record.id)
assertEquals(E164_A, record.e164)
assertEquals(ACI_A, record.sid)
assertEquals(PNI_A, record.pni)
}
@Test
fun processCdsV2Result_e164AndAciMatches() {
val inputId: RecipientId = insert(E164_A, null, ACI_A)
val resultId: RecipientId = recipientDatabase.processCdsV2Result(E164_A, PNI_A, ACI_A)
val record: IdRecord = require(resultId)
assertEquals(inputId, record.id)
assertEquals(E164_A, record.e164)
assertEquals(ACI_A, record.sid)
assertEquals(PNI_A, record.pni)
}
@Test
fun processCdsV2Result_onlyPniMatches() {
val inputId: RecipientId = insert(null, PNI_A, null)
val resultId: RecipientId = recipientDatabase.processCdsV2Result(E164_A, PNI_A, ACI_A)
val record: IdRecord = require(resultId)
assertEquals(inputId, record.id)
assertEquals(E164_A, record.e164)
assertEquals(ACI_A, record.sid)
assertEquals(PNI_A, record.pni)
}
@Test
fun processCdsV2Result_pniAndAciMatches() {
val inputId: RecipientId = insert(null, PNI_A, ACI_A)
val resultId: RecipientId = recipientDatabase.processCdsV2Result(E164_A, PNI_A, ACI_A)
val record: IdRecord = require(resultId)
assertEquals(inputId, record.id)
assertEquals(E164_A, record.e164)
assertEquals(ACI_A, record.sid)
assertEquals(PNI_A, record.pni)
}
@Test
fun processCdsV2Result_onlyAciMatches() {
val inputId: RecipientId = insert(null, null, ACI_A)
val resultId: RecipientId = recipientDatabase.processCdsV2Result(E164_A, PNI_A, ACI_A)
val record: IdRecord = require(resultId)
assertEquals(inputId, record.id)
assertEquals(E164_A, record.e164)
assertEquals(ACI_A, record.sid)
assertEquals(PNI_A, record.pni)
}
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))
}
}
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

@@ -4,7 +4,7 @@ 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;

View File

@@ -1,119 +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.internal.push.VerifyAccountResponse
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 : 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>()
for (i in 0..4) {
val aci = ACI.from(UUID.randomUUID())
val recipientId = RecipientId.from(aci, "+1555555101$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,60 @@
package org.thoughtcrime.securesms
import com.facebook.flipper.android.AndroidFlipperClient
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 leakcanary.LeakCanary
import org.thoughtcrime.securesms.database.FlipperSqlCipherAdapter
import shark.AndroidReferenceMatchers
class FlipperApplicationContext : ApplicationContext() {
override fun onCreate() {
super.onCreate()
SoLoader.init(this, false)
val client = AndroidFlipperClient.getInstance(this)
client.addPlugin(InspectorFlipperPlugin(this, DescriptorMapping.withDefaults()))
client.addPlugin(DatabasesFlipperPlugin(FlipperSqlCipherAdapter(this)))
client.addPlugin(SharedPreferencesFlipperPlugin(this))
client.start()
LeakCanary.config = LeakCanary.config.copy(
referenceMatchers = AndroidReferenceMatchers.appDefaults +
AndroidReferenceMatchers.ignoredInstanceField(
className = "android.service.media.MediaBrowserService\$ServiceBinder",
fieldName = "this\$0"
) +
AndroidReferenceMatchers.ignoredInstanceField(
className = "androidx.media.MediaBrowserServiceCompat\$MediaBrowserServiceImplApi26\$MediaBrowserServiceApi26",
fieldName = "mBase"
) +
AndroidReferenceMatchers.ignoredInstanceField(
className = "android.support.v4.media.MediaBrowserCompat",
fieldName = "mImpl"
) +
AndroidReferenceMatchers.ignoredInstanceField(
className = "android.support.v4.media.session.MediaControllerCompat",
fieldName = "mToken"
) +
AndroidReferenceMatchers.ignoredInstanceField(
className = "android.support.v4.media.session.MediaControllerCompat",
fieldName = "mImpl"
) +
AndroidReferenceMatchers.ignoredInstanceField(
className = "org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackService",
fieldName = "mApplication"
) +
AndroidReferenceMatchers.ignoredInstanceField(
className = "org.thoughtcrime.securesms.service.GenericForegroundService\$LocalBinder",
fieldName = "this\$0"
) +
AndroidReferenceMatchers.ignoredInstanceField(
className = "org.thoughtcrime.securesms.contacts.ContactsSyncAdapter",
fieldName = "mContext"
)
)
}
}

View File

@@ -0,0 +1,271 @@
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.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 {
SignalDatabaseOpenHelper mainOpenHelper = Objects.requireNonNull(SignalDatabase.getInstance());
SignalDatabaseOpenHelper keyValueOpenHelper = KeyValueDatabase.getInstance((Application) getContext());
SignalDatabaseOpenHelper megaphoneOpenHelper = MegaphoneDatabase.getInstance((Application) getContext());
SignalDatabaseOpenHelper jobManagerOpenHelper = JobDatabase.getInstance((Application) getContext());
SignalDatabaseOpenHelper 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 SignalDatabaseOpenHelper sqlCipherOpenHelper;
Descriptor(@NonNull SignalDatabaseOpenHelper 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

@@ -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"
@@ -179,12 +177,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 +221,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 +255,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 +272,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 +282,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 +308,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 +318,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" />
@@ -389,32 +394,6 @@
</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"/>
@@ -425,11 +404,6 @@
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 +428,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 +445,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 +522,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 +632,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 +679,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 +738,6 @@
<receiver android:name=".service.ExpirationListener" />
<receiver android:name=".service.ExpiringStoriesManager$ExpireStoriesAlarm" />
<receiver android:name=".revealable.ViewOnceMessageManager$ViewOnceAlarm" />
<receiver android:name=".service.PendingRetryReceiptManager$PendingRetryReceiptAlarm" />

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: 120 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 150 KiB

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 149 KiB

After

Width:  |  Height:  |  Size: 150 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: 148 KiB

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 142 KiB

After

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 174 KiB

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

After

Width:  |  Height:  |  Size: 106 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

@@ -52,11 +52,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));

View File

@@ -35,32 +35,29 @@ 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.thoughtcrime.securesms.emoji.JumboEmoji;
import org.thoughtcrime.securesms.mms.SignalGlideModule;
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.LogDatabase;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.database.SqlCipherLibraryLoader;
import org.thoughtcrime.securesms.database.SignalDatabase;
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.SubscriptionKeepAliveJob;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.logging.CustomSignalProtocolLogger;
@@ -68,7 +65,6 @@ 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.ratelimit.RateLimitUtil;
@@ -81,7 +77,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;
@@ -93,7 +88,9 @@ 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.whispersystems.libsignal.logging.SignalProtocolLoggerProvider;
import java.io.IOException;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.security.Security;
@@ -178,7 +175,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
.addNonBlocking(this::initializeRevealableMessageManager)
.addNonBlocking(this::initializePendingRetryReceiptManager)
.addNonBlocking(this::initializeFcmCheck)
.addNonBlocking(CreateSignedPreKeyJob::enqueueIfNeeded)
.addNonBlocking(this::initializeSignedPreKeyCheck)
.addNonBlocking(this::initializePeriodicTasks)
.addNonBlocking(this::initializeCircumvention)
.addNonBlocking(this::initializePendingMessages)
@@ -190,7 +187,6 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
.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)))
@@ -198,11 +194,6 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
.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)
.execute();
Log.d(TAG, "onCreate() took " + (System.currentTimeMillis() - startTime) + " ms");
@@ -218,7 +209,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
ApplicationDependencies.getFrameRateTracker().start();
ApplicationDependencies.getMegaphoneRepository().onAppForegrounded();
ApplicationDependencies.getDeadlockDetector().start();
SubscriptionKeepAliveJob.enqueueAndTrackTimeIfNecessary();
SubscriptionKeepAliveJob.launchSubscriberIdKeepAliveJobIfNecessary();
SignalExecutors.BOUNDED.execute(() -> {
FeatureFlags.refreshIfNecessary();
@@ -359,6 +350,12 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
}
}
private void initializeSignedPreKeyCheck() {
if (!TextSecurePreferences.isSignedPreKeyRegistered(this)) {
ApplicationDependencies.getJobManager().add(new CreateSignedPreKeyJob(this));
}
}
private void initializeExpiringMessageManager() {
ApplicationDependencies.getExpiringMessageManager().checkSchedule();
}

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();
@@ -61,14 +60,9 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable,
// 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);
@@ -98,15 +92,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,8 +1,8 @@
package org.thoughtcrime.securesms;
import androidx.annotation.NonNull;
import androidx.lifecycle.LifecycleOwner;
import org.thoughtcrime.securesms.conversationlist.model.Conversation;
import org.thoughtcrime.securesms.conversationlist.model.ConversationSet;
import org.thoughtcrime.securesms.database.model.ThreadRecord;
import org.thoughtcrime.securesms.mms.GlideRequests;
@@ -12,8 +12,7 @@ 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);

View File

@@ -13,7 +13,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import org.thoughtcrime.securesms.database.SignalDatabase;
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.
@@ -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);
@@ -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,11 +18,11 @@ 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;
@@ -31,6 +31,7 @@ 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 +42,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 +53,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 +132,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 +190,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 +239,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 +255,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 +285,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 +343,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 +491,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 +521,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 +546,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 +571,12 @@ public final class ContactSelectionListFragment extends LoggingFragment
AlertDialog loadingDialog = SimpleProgressDialog.show(requireContext());
SimpleTask.run(getViewLifecycleOwner().getLifecycle(), () -> {
return UsernameUtil.fetchAciForUsername(contact.getNumber());
return UsernameUtil.fetchAciForUsername(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 +646,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 +724,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 +759,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, @Nullable String number, Consumer<Boolean> callback);
void onContactDeselected(Optional<RecipientId> recipientId, @Nullable 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 +784,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;
@@ -185,13 +186,12 @@ 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);
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

@@ -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;
@@ -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,7 +250,7 @@ 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);

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,6 +69,7 @@ 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;
@@ -90,8 +89,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 +129,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 +161,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 +205,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 +288,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 +388,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);
}
}
@@ -564,27 +555,19 @@ 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);
if (oldAdapter == null || restartItem >= 0) {
int item = restartItem >= 0 ? restartItem : mediaPosition;
mediaPager.setCurrentItem(item);
viewModel.setCursor(this, cursor, leftIsRecent);
if (item == 0) {
viewPagerListener.onPageSelected(0);
}
int item = restartItem >= 0 ? restartItem : mediaPosition;
mediaPager.setCurrentItem(item);
if (item == 0) {
viewPagerListener.onPageSelected(0);
}
} else {
mediaNotAvailable();
@@ -608,15 +591,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
@@ -741,10 +715,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 +739,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;

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;

View File

@@ -21,11 +21,10 @@ 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.groups.ui.creategroup.CreateGroupActivity;
@@ -33,11 +32,11 @@ 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.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 (SignalStore.account().isRegistered() && 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.hasAci()) {
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.");

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

@@ -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,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

@@ -39,19 +39,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;
@@ -78,19 +75,16 @@ 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 {
@@ -98,7 +92,6 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
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";
@@ -114,13 +107,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);
@@ -163,16 +153,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()");
@@ -213,8 +193,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();
@@ -307,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);
@@ -317,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);
@@ -524,11 +501,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));
@@ -651,8 +623,6 @@ 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:
@@ -689,17 +659,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) {
@@ -848,7 +807,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

@@ -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

@@ -12,6 +12,7 @@ import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.components.voice.VoiceNoteDraft;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
import org.thoughtcrime.securesms.util.concurrent.SettableFuture;
@@ -51,7 +52,7 @@ public class AudioRecorder {
.withMimeType(MediaUtil.AUDIO_AAC)
.createForDraftAttachmentAsync(context, () -> Log.i(TAG, "Write successful."), e -> Log.w(TAG, "Error during recording", e));
recorder = Build.VERSION.SDK_INT >= 26 ? new MediaRecorderWrapper() : new AudioCodec();
recorder = Build.VERSION.SDK_INT >= 26 && FeatureFlags.voiceNoteRecordingV2() ? new MediaRecorderWrapper() : new AudioCodec();
recorder.start(fds[1]);
} catch (IOException e) {
Log.w(TAG, e);

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()
}
}

View File

@@ -31,8 +31,8 @@ class TextAvatarDrawable(
}
override fun draw(canvas: Canvas) {
val textSize = Avatars.getTextSizeForLength(context, avatar.text, size * 0.8f, size * 0.45f)
val width = bounds.width()
val textSize = Avatars.getTextSizeForLength(context, avatar.text, width * 0.8f, width * 0.45f)
val candidates = EmojiProvider.getCandidates(avatar.text)
textPaint.textSize = textSize

View File

@@ -81,7 +81,14 @@ class AvatarPickerFragment : Fragment(R.layout.avatar_picker_fragment) {
}
clearButton.visible = state.canClear
saveButton.isClickable = state.canSave
val wasEnabled = saveButton.isEnabled
saveButton.isEnabled = state.canSave
if (wasEnabled != state.canSave) {
val alpha = if (state.canSave) 1f else 0.5f
saveButton.animate().cancel()
saveButton.animate().alpha(alpha)
}
val items = state.selectableAvatars.map { AvatarPickerItem.Model(it, it == state.currentAvatar) }
val selectedPosition = items.indexOfFirst { it.isSelected }
@@ -97,11 +104,6 @@ class AvatarPickerFragment : Fragment(R.layout.avatar_picker_fragment) {
photoButton.setOnIconClickedListener { openGallery() }
textButton.setOnIconClickedListener { openTextEditor(null) }
saveButton.setOnClickListener { v ->
if (!saveButton.isEnabled) {
return@setOnClickListener
}
saveButton.isEnabled = false
viewModel.save(
{
setFragmentResult(
@@ -242,8 +244,4 @@ class AvatarPickerFragment : Fragment(R.layout.avatar_picker_fragment) {
}
.execute()
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults)
}
}

View File

@@ -183,7 +183,7 @@ sealed class AvatarPickerViewModel(private val repository: AvatarPickerRepositor
private val isNewGroup: Boolean,
private val groupAvatarMedia: Media?
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
val viewModel = if (groupId == null && !isNewGroup) {
SelfAvatarPickerViewModel(repository)
} else if (groupId == null) {

View File

@@ -33,7 +33,7 @@ class TextAvatarCreationViewModel(initialText: Avatar.Text) : ViewModel() {
}
class Factory(private val initialText: Avatar.Text) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return requireNotNull(modelClass.cast(TextAvatarCreationViewModel(initialText)))
}
}

View File

@@ -20,7 +20,7 @@ class VectorAvatarCreationViewModel(initialAvatar: Avatar.Vector) : ViewModel()
fun getCurrentAvatar() = store.state.currentAvatar
class Factory(private val initialAvatar: Avatar.Vector) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return requireNotNull(modelClass.cast(VectorAvatarCreationViewModel(initialAvatar)))
}
}

View File

@@ -1,94 +0,0 @@
package org.thoughtcrime.securesms.avatar.view
import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.widget.FrameLayout
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.AvatarImageView
import org.thoughtcrime.securesms.database.model.StoryViewState
import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.stories.Stories
import org.thoughtcrime.securesms.util.visible
/**
* AvatarView encapsulating the AvatarImageView and decorations.
*/
class AvatarView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null
) : FrameLayout(context, attrs) {
init {
inflate(context, R.layout.avatar_view, this)
isClickable = false
}
private val avatar: AvatarImageView = findViewById<AvatarImageView>(R.id.avatar_image_view).apply {
initialize(context, attrs)
}
private val storyRing: View = findViewById(R.id.avatar_story_ring)
private fun showStoryRing(hasUnreadStory: Boolean) {
if (!Stories.isFeatureEnabled()) {
return
}
storyRing.visible = true
storyRing.isActivated = hasUnreadStory
avatar.scaleX = 0.8f
avatar.scaleY = 0.8f
}
private fun hideStoryRing() {
storyRing.visible = false
avatar.scaleX = 1f
avatar.scaleY = 1f
}
fun hasStory(): Boolean {
return storyRing.visible
}
fun setStoryRingFromState(storyViewState: StoryViewState) {
when (storyViewState) {
StoryViewState.NONE -> hideStoryRing()
StoryViewState.UNVIEWED -> showStoryRing(true)
StoryViewState.VIEWED -> showStoryRing(false)
}
}
/**
* Displays Note-to-Self
*/
fun displayChatAvatar(recipient: Recipient) {
avatar.setAvatar(recipient)
}
/**
* Displays Note-to-Self
*/
fun displayChatAvatar(requestManager: GlideRequests, recipient: Recipient, isQuickContactEnabled: Boolean) {
avatar.setAvatar(requestManager, recipient, isQuickContactEnabled)
}
/**
* Displays Profile image
*/
fun displayProfileAvatar(recipient: Recipient) {
avatar.setRecipient(recipient)
}
fun setFallbackPhotoProvider(fallbackPhotoProvider: Recipient.FallbackPhotoProvider) {
avatar.setFallbackPhotoProvider(fallbackPhotoProvider)
}
fun disableQuickContact() {
avatar.disableQuickContact()
}
}

View File

@@ -23,8 +23,6 @@ import androidx.annotation.RequiresApi;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.Fragment;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
@@ -112,7 +110,7 @@ public class BackupDialog {
@RequiresApi(29)
public static void showChooseBackupLocationDialog(@NonNull Fragment fragment, int requestCode) {
new MaterialAlertDialogBuilder(fragment.requireContext())
new AlertDialog.Builder(fragment.requireContext())
.setView(R.layout.backup_choose_location_dialog)
.setCancelable(true)
.setNegativeButton(android.R.string.cancel, (dialog, which) -> {
@@ -143,7 +141,7 @@ public class BackupDialog {
}
public static void showDisableBackupDialog(@NonNull Context context, @NonNull Runnable onBackupsDisabled) {
new MaterialAlertDialogBuilder(context)
new AlertDialog.Builder(context)
.setTitle(R.string.BackupDialog_delete_backups)
.setMessage(R.string.BackupDialog_disable_and_delete_all_local_backups)
.setNegativeButton(android.R.string.cancel, null)
@@ -159,12 +157,12 @@ public class BackupDialog {
public static void showVerifyBackupPassphraseDialog(@NonNull Context context) {
View view = LayoutInflater.from(context).inflate(R.layout.enter_backup_passphrase_dialog, null);
EditText prompt = view.findViewById(R.id.restore_passphrase_input);
AlertDialog dialog = new MaterialAlertDialogBuilder(context)
.setTitle(R.string.BackupDialog_enter_backup_passphrase_to_verify)
.setView(view)
.setPositiveButton(R.string.BackupDialog_verify, null)
.setNegativeButton(android.R.string.cancel, null)
.show();
AlertDialog dialog = new AlertDialog.Builder(context)
.setTitle(R.string.BackupDialog_enter_backup_passphrase_to_verify)
.setView(view)
.setPositiveButton(R.string.BackupDialog_verify, null)
.setNegativeButton(android.R.string.cancel, null)
.show();
Button positiveButton = dialog.getButton(AlertDialog.BUTTON_POSITIVE);
positiveButton.setEnabled(false);

View File

@@ -5,7 +5,8 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.greenrobot.eventbus.EventBus;
import org.signal.libsignal.protocol.util.ByteUtil;
import org.signal.core.util.logging.Log;
import org.whispersystems.libsignal.util.ByteUtil;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

View File

@@ -18,11 +18,10 @@ import net.zetetic.database.sqlcipher.SQLiteDatabase;
import org.greenrobot.eventbus.EventBus;
import org.signal.core.util.Conversions;
import org.signal.core.util.logging.Log;
import org.signal.libsignal.protocol.kdf.HKDFv3;
import org.signal.libsignal.protocol.util.ByteUtil;
import org.thoughtcrime.securesms.attachments.AttachmentId;
import org.thoughtcrime.securesms.crypto.AttachmentSecret;
import org.thoughtcrime.securesms.crypto.ClassicDecryptingPartInputStream;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
import org.thoughtcrime.securesms.crypto.ModernDecryptingPartInputStream;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.database.EmojiSearchDatabase;
@@ -47,12 +46,13 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.keyvalue.KeyValueDataSet;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.profiles.AvatarHelper;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.signal.core.util.CursorUtil;
import org.signal.core.util.SetUtil;
import org.thoughtcrime.securesms.util.CursorUtil;
import org.thoughtcrime.securesms.util.SetUtil;
import org.thoughtcrime.securesms.util.Stopwatch;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.kdf.HKDFv3;
import org.whispersystems.libsignal.util.ByteUtil;
import java.io.File;
import java.io.FileOutputStream;
@@ -162,17 +162,17 @@ public class FullBackupExporter extends FullBackupBase {
for (String table : tables) {
throwIfCanceled(cancellationSignal);
if (table.equals(MmsDatabase.TABLE_NAME)) {
count = exportTable(table, input, outputStream, cursor -> isNonExpiringMmsMessage(cursor) && isNotReleaseChannel(cursor), null, count, estimatedCount, cancellationSignal);
count = exportTable(table, input, outputStream, FullBackupExporter::isNonExpiringMmsMessage, null, count, estimatedCount, cancellationSignal);
} else if (table.equals(SmsDatabase.TABLE_NAME)) {
count = exportTable(table, input, outputStream, cursor -> isNonExpiringSmsMessage(cursor) && isNotReleaseChannel(cursor), null, count, estimatedCount, cancellationSignal);
count = exportTable(table, input, outputStream, FullBackupExporter::isNonExpiringSmsMessage, null, count, estimatedCount, cancellationSignal);
} else if (table.equals(ReactionDatabase.TABLE_NAME)) {
count = exportTable(table, input, outputStream, cursor -> isForNonExpiringMessage(input, new MessageId(CursorUtil.requireLong(cursor, ReactionDatabase.MESSAGE_ID), CursorUtil.requireBoolean(cursor, ReactionDatabase.IS_MMS))), null, count, estimatedCount, cancellationSignal);
} else if (table.equals(MentionDatabase.TABLE_NAME)) {
count = exportTable(table, input, outputStream, cursor -> isForNonExpiringMmsMessageAndNotReleaseChannel(input, CursorUtil.requireLong(cursor, MentionDatabase.MESSAGE_ID)), null, count, estimatedCount, cancellationSignal);
count = exportTable(table, input, outputStream, cursor -> isForNonExpiringMmsMessage(input, CursorUtil.requireLong(cursor, MentionDatabase.MESSAGE_ID)), null, count, estimatedCount, cancellationSignal);
} else if (table.equals(GroupReceiptDatabase.TABLE_NAME)) {
count = exportTable(table, input, outputStream, cursor -> isForNonExpiringMmsMessageAndNotReleaseChannel(input, cursor.getLong(cursor.getColumnIndexOrThrow(GroupReceiptDatabase.MMS_ID))), null, count, estimatedCount, cancellationSignal);
count = exportTable(table, input, outputStream, cursor -> isForNonExpiringMmsMessage(input, cursor.getLong(cursor.getColumnIndexOrThrow(GroupReceiptDatabase.MMS_ID))), null, count, estimatedCount, cancellationSignal);
} else if (table.equals(AttachmentDatabase.TABLE_NAME)) {
count = exportTable(table, input, outputStream, cursor -> isForNonExpiringMmsMessageAndNotReleaseChannel(input, cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.MMS_ID))), (cursor, innerCount) -> exportAttachment(attachmentSecret, cursor, outputStream, innerCount, estimatedCount), count, estimatedCount, cancellationSignal);
count = exportTable(table, input, outputStream, cursor -> isForNonExpiringMmsMessage(input, cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.MMS_ID))), (cursor, innerCount) -> exportAttachment(attachmentSecret, cursor, outputStream, innerCount, estimatedCount), count, estimatedCount, cancellationSignal);
} else if (table.equals(StickerDatabase.TABLE_NAME)) {
count = exportTable(table, input, outputStream, cursor -> true, (cursor, innerCount) -> exportSticker(attachmentSecret, cursor, outputStream, innerCount, estimatedCount), count, estimatedCount, cancellationSignal);
} else if (!BLACKLISTED_TABLES.contains(table) && !table.startsWith("sqlite_")) {
@@ -181,6 +181,12 @@ public class FullBackupExporter extends FullBackupBase {
stopwatch.split("table::" + table);
}
for (BackupProtos.SharedPreference preference : IdentityKeyUtil.getBackupRecord(context)) {
throwIfCanceled(cancellationSignal);
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count, estimatedCount));
outputStream.write(preference);
}
for (BackupProtos.SharedPreference preference : TextSecurePreferences.getPreferencesToSaveToBackup(context)) {
throwIfCanceled(cancellationSignal);
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count, estimatedCount));
@@ -197,9 +203,7 @@ public class FullBackupExporter extends FullBackupBase {
throwIfCanceled(cancellationSignal);
if (avatar != null) {
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count, estimatedCount));
try (InputStream inputStream = avatar.getInputStream()) {
outputStream.write(avatar.getFilename(), inputStream, avatar.getLength());
}
outputStream.write(avatar.getFilename(), avatar.getInputStream(), avatar.getLength());
}
}
@@ -379,7 +383,6 @@ public class FullBackupExporter extends FullBackupBase {
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count, estimatedCount));
outputStream.write(new AttachmentId(rowId, uniqueId), inputStream, size);
inputStream.close();
}
} catch (IOException e) {
Log.w(TAG, e);
@@ -398,9 +401,8 @@ public class FullBackupExporter extends FullBackupBase {
if (!TextUtils.isEmpty(data) && size > 0) {
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count, estimatedCount));
try (InputStream inputStream = ModernDecryptingPartInputStream.createFor(attachmentSecret, random, new File(data), 0)) {
outputStream.writeSticker(rowId, inputStream, size);
}
InputStream inputStream = ModernDecryptingPartInputStream.createFor(attachmentSecret, random, new File(data), 0);
outputStream.writeSticker(rowId, inputStream, size);
}
} catch (IOException e) {
Log.w(TAG, e);
@@ -445,12 +447,7 @@ public class FullBackupExporter extends FullBackupBase {
Class<?> type = dataSet.getType(key);
if (type == byte[].class) {
byte[] data = dataSet.getBlob(key, null);
if (data != null) {
builder.setBlobValue(ByteString.copyFrom(dataSet.getBlob(key, null)));
} else {
Log.w(TAG, "Skipping storing null blob for key: " + key);
}
builder.setBlobValue(ByteString.copyFrom(dataSet.getBlob(key, null)));
} else if (type == Boolean.class) {
builder.setBooleanValue(dataSet.getBoolean(key, false));
} else if (type == Float.class) {
@@ -460,12 +457,7 @@ public class FullBackupExporter extends FullBackupBase {
} else if (type == Long.class) {
builder.setLongValue(dataSet.getLong(key, 0));
} else if (type == String.class) {
String data = dataSet.getString(key, null);
if (data != null) {
builder.setStringValue(dataSet.getString(key, null));
} else {
Log.w(TAG, "Skipping storing null string for key: " + key);
}
builder.setStringValue(dataSet.getString(key, null));
} else {
throw new AssertionError("Unknown type: " + type);
}
@@ -488,7 +480,7 @@ public class FullBackupExporter extends FullBackupBase {
private static boolean isForNonExpiringMessage(@NonNull SQLiteDatabase db, @NonNull MessageId messageId) {
if (messageId.isMms()) {
return isForNonExpiringMmsMessageAndNotReleaseChannel(db, messageId.getId());
return isForNonExpiringMmsMessage(db, messageId.getId());
} else {
return isForNonExpiringSmsMessage(db, messageId.getId());
}
@@ -508,24 +500,20 @@ public class FullBackupExporter extends FullBackupBase {
return false;
}
private static boolean isForNonExpiringMmsMessageAndNotReleaseChannel(@NonNull SQLiteDatabase db, long mmsId) {
String[] columns = new String[] { MmsDatabase.RECIPIENT_ID, MmsDatabase.EXPIRES_IN, MmsDatabase.VIEW_ONCE};
private static boolean isForNonExpiringMmsMessage(@NonNull SQLiteDatabase db, long mmsId) {
String[] columns = new String[] { MmsDatabase.EXPIRES_IN, MmsDatabase.VIEW_ONCE};
String where = MmsDatabase.ID + " = ?";
String[] args = new String[] { String.valueOf(mmsId) };
try (Cursor mmsCursor = db.query(MmsDatabase.TABLE_NAME, columns, where, args, null, null, null)) {
if (mmsCursor != null && mmsCursor.moveToFirst()) {
return isNonExpiringMmsMessage(mmsCursor) && isNotReleaseChannel(mmsCursor);
return isNonExpiringMmsMessage(mmsCursor);
}
}
return false;
}
private static boolean isNotReleaseChannel(Cursor cursor) {
RecipientId releaseChannel = SignalStore.releaseChannelValues().getReleaseChannelRecipientId();
return releaseChannel == null || cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.RECIPIENT_ID)) != releaseChannel.toLong();
}
private static class BackupFrameOutputStream extends BackupStream {

View File

@@ -17,8 +17,6 @@ import org.greenrobot.eventbus.EventBus;
import org.signal.core.util.Conversions;
import org.signal.core.util.StreamUtil;
import org.signal.core.util.logging.Log;
import org.signal.libsignal.protocol.kdf.HKDFv3;
import org.signal.libsignal.protocol.util.ByteUtil;
import org.thoughtcrime.securesms.backup.BackupProtos.Attachment;
import org.thoughtcrime.securesms.backup.BackupProtos.BackupFrame;
import org.thoughtcrime.securesms.backup.BackupProtos.DatabaseVersion;
@@ -34,11 +32,12 @@ import org.thoughtcrime.securesms.database.SearchDatabase;
import org.thoughtcrime.securesms.database.StickerDatabase;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.keyvalue.KeyValueDataSet;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.profiles.AvatarHelper;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.BackupUtil;
import org.signal.core.util.SqlUtil;
import org.thoughtcrime.securesms.util.SqlUtil;
import org.whispersystems.libsignal.kdf.HKDFv3;
import org.whispersystems.libsignal.util.ByteUtil;
import java.io.ByteArrayOutputStream;
import java.io.File;
@@ -69,18 +68,6 @@ public class FullBackupImporter extends FullBackupBase {
@SuppressWarnings("unused")
private static final String TAG = Log.tag(FullBackupImporter.class);
private static final String[] TABLES_TO_DROP_FIRST = {
"distribution_list_member",
"distribution_list",
"message_send_log_recipients",
"msl_recipient",
"msl_message",
"reaction",
"notification_profile_schedule",
"notification_profile_allowed_members",
"story_sends"
};
public static void importFile(@NonNull Context context, @NonNull AttachmentSecret attachmentSecret,
@NonNull SQLiteDatabase db, @NonNull Uri uri, @NonNull String passphrase)
throws IOException
@@ -97,12 +84,12 @@ public class FullBackupImporter extends FullBackupBase {
int count = 0;
SQLiteDatabase keyValueDatabase = KeyValueDatabase.getInstance(ApplicationDependencies.getApplication()).getSqlCipherDatabase();
db.beginTransaction();
keyValueDatabase.beginTransaction();
try {
BackupRecordInputStream inputStream = new BackupRecordInputStream(is, passphrase);
db.beginTransaction();
keyValueDatabase.beginTransaction();
dropAllTables(db);
BackupFrame frame;
@@ -220,7 +207,7 @@ public class FullBackupImporter extends FullBackupBase {
private static void processAvatar(@NonNull Context context, @NonNull SQLiteDatabase db, @NonNull BackupProtos.Avatar avatar, @NonNull BackupRecordInputStream inputStream) throws IOException {
if (avatar.hasRecipientId()) {
RecipientId recipientId = RecipientId.from(avatar.getRecipientId());
inputStream.readAttachmentTo(AvatarHelper.getOutputStream(context, recipientId, false), avatar.getLength());
inputStream.readAttachmentTo(AvatarHelper.getOutputStream(context, recipientId), avatar.getLength());
} else {
if (avatar.hasName() && SqlUtil.tableExists(db, "recipient_preferences")) {
Log.w(TAG, "Avatar is missing a recipientId. Clearing signal_profile_avatar (legacy) so it can be fetched later.");
@@ -263,17 +250,6 @@ public class FullBackupImporter extends FullBackupBase {
private static void processPreference(@NonNull Context context, SharedPreference preference) {
SharedPreferences preferences = context.getSharedPreferences(preference.getFile(), 0);
// Identity keys were moved from shared prefs into SignalStore. Need to handle importing backups made before the migration.
if ("SecureSMS-Preferences".equals(preference.getFile())) {
if ("pref_identity_public_v3".equals(preference.getKey()) && preference.hasValue()) {
SignalStore.account().restoreLegacyIdentityPublicKeyFromBackup(preference.getValue());
} else if ("pref_identity_private_v3".equals(preference.getKey()) && preference.hasValue()) {
SignalStore.account().restoreLegacyIdentityPrivateKeyFromBackup(preference.getValue());
}
return;
}
if (preference.hasValue()) {
preferences.edit().putString(preference.getKey(), preference.getValue()).commit();
} else if (preference.hasBooleanValue()) {
@@ -284,17 +260,12 @@ public class FullBackupImporter extends FullBackupBase {
}
private static void dropAllTables(@NonNull SQLiteDatabase db) {
for (String name : TABLES_TO_DROP_FIRST) {
db.execSQL("DROP TABLE IF EXISTS " + name);
}
try (Cursor cursor = db.rawQuery("SELECT name, type FROM sqlite_master", null)) {
while (cursor != null && cursor.moveToNext()) {
String name = cursor.getString(0);
String type = cursor.getString(1);
if ("table".equals(type) && !name.startsWith("sqlite_")) {
Log.i(TAG, "Dropping table: " + name);
db.execSQL("DROP TABLE IF EXISTS " + name);
}
}

View File

@@ -8,13 +8,11 @@ import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.badges.glide.BadgeSpriteTransformation
import org.thoughtcrime.securesms.badges.models.Badge
import org.thoughtcrime.securesms.database.model.databaseprotos.GiftBadge
import org.thoughtcrime.securesms.glide.GiftBadgeModel
import org.thoughtcrime.securesms.mms.GlideApp
import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.ScreenDensity
import org.thoughtcrime.securesms.util.ThemeUtil
import java.lang.IllegalArgumentException
class BadgeImageView @JvmOverloads constructor(
context: Context,
@@ -80,20 +78,6 @@ class BadgeImageView @JvmOverloads constructor(
}
}
fun setGiftBadge(badge: GiftBadge?, glideRequests: GlideRequests) {
if (badge != null) {
glideRequests
.load(GiftBadgeModel(badge))
.downsample(DownsampleStrategy.NONE)
.transform(BadgeSpriteTransformation(BadgeSpriteTransformation.Size.fromInteger(badgeSize), ScreenDensity.getBestDensityBucketForDevice(), ThemeUtil.isDarkTheme(context)))
.into(this)
} else {
glideRequests
.clear(this)
clearDrawable()
}
}
private fun clearDrawable() {
setImageDrawable(null)
isClickable = false

View File

@@ -1,83 +1,41 @@
package org.thoughtcrime.securesms.badges
import android.content.Context
import androidx.annotation.WorkerThread
import io.reactivex.rxjava3.core.Completable
import io.reactivex.rxjava3.schedulers.Schedulers
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.badges.models.Badge
import org.thoughtcrime.securesms.database.RecipientDatabase
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.jobs.MultiDeviceProfileContentUpdateJob
import org.thoughtcrime.securesms.jobs.RefreshOwnProfileJob
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.storage.StorageSyncHelper
import org.thoughtcrime.securesms.util.ProfileUtil
import java.io.IOException
class BadgeRepository(context: Context) {
companion object {
private val TAG = Log.tag(BadgeRepository::class.java)
}
private val context = context.applicationContext
/**
* Sets the visibility for each badge on a user's profile, and uploads them to the server.
* Does not write to the local database. The caller must either do that themselves or schedule
* a refresh own profile job.
*
* @return A list of the badges, properly modified to either visible or not visible, according to user preferences.
*/
@Throws(IOException::class)
@WorkerThread
fun setVisibilityForAllBadgesSync(
displayBadgesOnProfile: Boolean,
selfBadges: List<Badge>
): List<Badge> {
Log.d(TAG, "[setVisibilityForAllBadgesSync] Setting badge visibility...", true)
val recipientDatabase: RecipientDatabase = SignalDatabase.recipients
val badges = selfBadges.map { it.copy(visible = displayBadgesOnProfile) }
Log.d(TAG, "[setVisibilityForAllBadgesSync] Uploading profile...", true)
ProfileUtil.uploadProfileWithBadges(context, badges)
SignalStore.donationsValues().setDisplayBadgesOnProfile(displayBadgesOnProfile)
recipientDatabase.markNeedsSync(Recipient.self().id)
Log.d(TAG, "[setVisibilityForAllBadgesSync] Requesting data change sync...", true)
StorageSyncHelper.scheduleSyncForDataChange()
return badges
}
fun setVisibilityForAllBadges(
displayBadgesOnProfile: Boolean,
selfBadges: List<Badge> = Recipient.self().badges
): Completable = Completable.fromAction {
setVisibilityForAllBadgesSync(displayBadgesOnProfile, selfBadges)
val recipientDatabase: RecipientDatabase = SignalDatabase.recipients
val badges = selfBadges.map { it.copy(visible = displayBadgesOnProfile) }
Log.d(TAG, "[setVisibilityForAllBadges] Enqueueing profile refresh...", true)
ApplicationDependencies.getJobManager()
.startChain(RefreshOwnProfileJob())
.then(MultiDeviceProfileContentUpdateJob())
.enqueue()
ProfileUtil.uploadProfileWithBadges(context, badges)
SignalStore.donationsValues().setDisplayBadgesOnProfile(displayBadgesOnProfile)
recipientDatabase.markNeedsSync(Recipient.self().id)
StorageSyncHelper.scheduleSyncForDataChange()
recipientDatabase.setBadges(Recipient.self().id, badges)
}.subscribeOn(Schedulers.io())
fun setFeaturedBadge(featuredBadge: Badge): Completable = Completable.fromAction {
val badges = Recipient.self().badges
val reOrderedBadges = listOf(featuredBadge.copy(visible = true)) + (badges.filterNot { it.id == featuredBadge.id })
Log.d(TAG, "[setFeaturedBadge] Uploading profile with reordered badges...", true)
ProfileUtil.uploadProfileWithBadges(context, reOrderedBadges)
Log.d(TAG, "[setFeaturedBadge] Enqueueing profile refresh...", true)
ApplicationDependencies.getJobManager()
.startChain(RefreshOwnProfileJob())
.then(MultiDeviceProfileContentUpdateJob())
.enqueue()
val recipientDatabase: RecipientDatabase = SignalDatabase.recipients
recipientDatabase.setBadges(Recipient.self().id, reOrderedBadges)
}.subscribeOn(Schedulers.io())
}

View File

@@ -9,17 +9,16 @@ import com.google.android.flexbox.FlexboxLayoutManager
import com.google.android.flexbox.JustifyContent
import org.signal.core.util.DimensionUnit
import org.signal.core.util.logging.Log
import org.signal.libsignal.protocol.util.Pair
import org.thoughtcrime.securesms.BuildConfig
import org.thoughtcrime.securesms.badges.models.Badge
import org.thoughtcrime.securesms.badges.models.Badge.Category.Companion.fromCode
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
import org.thoughtcrime.securesms.database.model.databaseprotos.BadgeList
import org.thoughtcrime.securesms.util.ScreenDensity
import org.whispersystems.libsignal.util.Pair
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile
import java.math.BigDecimal
import java.sql.Timestamp
import java.util.concurrent.TimeUnit
object Badges {
@@ -94,8 +93,7 @@ object Badges {
Uri.parse(badge.imageUrl),
badge.imageDensity,
badge.expiration,
badge.visible,
0L
badge.visible
)
}
@@ -124,8 +122,7 @@ object Badges {
uriAndDensity.first(),
uriAndDensity.second(),
serviceBadge.expiration?.let { getTimestamp(it) } ?: 0,
serviceBadge.isVisible,
TimeUnit.SECONDS.toMillis(serviceBadge.duration)
serviceBadge.isVisible
)
}
}

View File

@@ -1,53 +0,0 @@
package org.thoughtcrime.securesms.badges.gifts
import android.os.Bundle
import androidx.fragment.app.FragmentManager
import org.thoughtcrime.securesms.badges.gifts.ExpiredGiftSheetConfiguration.forExpiredBadge
import org.thoughtcrime.securesms.badges.models.Badge
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
import org.thoughtcrime.securesms.components.settings.DSLSettingsBottomSheetFragment
import org.thoughtcrime.securesms.components.settings.configure
import org.thoughtcrime.securesms.util.BottomSheetUtil
import org.thoughtcrime.securesms.util.fragments.requireListener
/**
* Displays expired gift information and gives the user the option to start a recurring monthly donation.
*/
class ExpiredGiftSheet : DSLSettingsBottomSheetFragment() {
companion object {
private const val ARG_BADGE = "arg.badge"
fun show(fragmentManager: FragmentManager, badge: Badge) {
ExpiredGiftSheet().apply {
arguments = Bundle().apply {
putParcelable(ARG_BADGE, badge)
}
}.show(fragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG)
}
}
private val badge: Badge
get() = requireArguments().getParcelable(ARG_BADGE)!!
override fun bindAdapter(adapter: DSLSettingsAdapter) {
ExpiredGiftSheetConfiguration.register(adapter)
adapter.submitList(
configure {
forExpiredBadge(
badge = badge,
onMakeAMonthlyDonation = {
requireListener<Callback>().onMakeAMonthlyDonation()
},
onNotNow = {
dismissAllowingStateLoss()
}
)
}.toMappingModelList()
)
}
interface Callback {
fun onMakeAMonthlyDonation()
}
}

View File

@@ -1,81 +0,0 @@
package org.thoughtcrime.securesms.badges.gifts
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.badges.models.Badge
import org.thoughtcrime.securesms.badges.models.BadgeDisplay112
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
import org.thoughtcrime.securesms.database.model.databaseprotos.GiftBadge
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
/**
* Contains shared DSL layout for expired gifts, creatable using a GiftBadge or a Badge.
*/
object ExpiredGiftSheetConfiguration {
fun register(mappingAdapter: MappingAdapter) {
BadgeDisplay112.register(mappingAdapter)
}
fun DSLConfiguration.forExpiredBadge(badge: Badge, onMakeAMonthlyDonation: () -> Unit, onNotNow: () -> Unit) {
customPref(BadgeDisplay112.Model(badge, withDisplayText = false))
expiredSheet(onMakeAMonthlyDonation, onNotNow)
}
fun DSLConfiguration.forExpiredGiftBadge(giftBadge: GiftBadge, onMakeAMonthlyDonation: () -> Unit, onNotNow: () -> Unit) {
customPref(BadgeDisplay112.GiftModel(giftBadge))
expiredSheet(onMakeAMonthlyDonation, onNotNow)
}
private fun DSLConfiguration.expiredSheet(onMakeAMonthlyDonation: () -> Unit, onNotNow: () -> Unit) {
textPref(
title = DSLSettingsText.from(
stringId = R.string.ExpiredGiftSheetConfiguration__your_gift_badge_has_expired,
DSLSettingsText.CenterModifier, DSLSettingsText.TitleLargeModifier
)
)
textPref(
title = DSLSettingsText.from(
stringId = R.string.ExpiredGiftSheetConfiguration__your_gift_badge_has_expired_and_is,
DSLSettingsText.CenterModifier
)
)
if (SignalStore.donationsValues().isLikelyASustainer()) {
primaryButton(
text = DSLSettingsText.from(
stringId = android.R.string.ok
),
onClick = {
onNotNow()
}
)
} else {
textPref(
title = DSLSettingsText.from(
stringId = R.string.ExpiredGiftSheetConfiguration__to_continue,
DSLSettingsText.CenterModifier
)
)
primaryButton(
text = DSLSettingsText.from(
stringId = R.string.ExpiredGiftSheetConfiguration__make_a_monthly_donation
),
onClick = {
onMakeAMonthlyDonation()
}
)
secondaryButtonNoOutline(
text = DSLSettingsText.from(
stringId = R.string.ExpiredGiftSheetConfiguration__not_now
),
onClick = {
onNotNow()
}
)
}
}
}

View File

@@ -1,111 +0,0 @@
package org.thoughtcrime.securesms.badges.gifts
import android.content.Context
import android.content.res.ColorStateList
import android.graphics.Color
import android.util.AttributeSet
import android.widget.FrameLayout
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.core.content.res.use
import androidx.swiperefreshlayout.widget.CircularProgressDrawable
import com.google.android.material.button.MaterialButton
import org.signal.core.util.DimensionUnit
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.badges.BadgeImageView
import org.thoughtcrime.securesms.badges.gifts.Gifts.formatExpiry
import org.thoughtcrime.securesms.database.model.databaseprotos.GiftBadge
import org.thoughtcrime.securesms.mms.GlideRequests
/**
* Displays a gift badge sent to or received from a user, and allows the user to
* perform an action based off the badge's redemption state.
*/
class GiftMessageView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null
) : FrameLayout(context, attrs) {
init {
inflate(context, R.layout.gift_message_view, this)
}
private val badgeView: BadgeImageView = findViewById(R.id.gift_message_view_badge)
private val titleView: TextView = findViewById(R.id.gift_message_view_title)
private val descriptionView: TextView = findViewById(R.id.gift_message_view_description)
private val actionView: MaterialButton = findViewById(R.id.gift_message_view_action)
init {
context.obtainStyledAttributes(attrs, R.styleable.GiftMessageView).use {
val textColor = it.getColor(R.styleable.GiftMessageView_giftMessageView__textColor, Color.RED)
titleView.setTextColor(textColor)
descriptionView.setTextColor(textColor)
val buttonTextColor = it.getColor(R.styleable.GiftMessageView_giftMessageView__buttonTextColor, Color.RED)
actionView.setTextColor(buttonTextColor)
actionView.iconTint = ColorStateList.valueOf(buttonTextColor)
val buttonBackgroundTint = it.getColor(R.styleable.GiftMessageView_giftMessageView__buttonBackgroundTint, Color.RED)
actionView.backgroundTintList = ColorStateList.valueOf(buttonBackgroundTint)
}
}
fun setGiftBadge(glideRequests: GlideRequests, giftBadge: GiftBadge, isOutgoing: Boolean, callback: Callback) {
titleView.setText(R.string.GiftMessageView__gift_badge)
descriptionView.text = giftBadge.formatExpiry(context)
actionView.icon = null
actionView.setOnClickListener { callback.onViewGiftBadgeClicked() }
actionView.isEnabled = true
if (isOutgoing) {
actionView.setText(R.string.GiftMessageView__view)
} else {
when (giftBadge.redemptionState) {
GiftBadge.RedemptionState.REDEEMED -> {
stopAnimationIfNeeded()
actionView.setIconResource(R.drawable.ic_check_circle_24)
}
GiftBadge.RedemptionState.STARTED -> actionView.icon = CircularProgressDrawable(context).apply {
actionView.isEnabled = false
setColorSchemeColors(ContextCompat.getColor(context, R.color.core_ultramarine))
strokeWidth = DimensionUnit.DP.toPixels(2f)
start()
}
else -> {
stopAnimationIfNeeded()
actionView.icon = null
}
}
actionView.setText(
when (giftBadge.redemptionState ?: GiftBadge.RedemptionState.UNRECOGNIZED) {
GiftBadge.RedemptionState.PENDING -> R.string.GiftMessageView__redeem
GiftBadge.RedemptionState.STARTED -> R.string.GiftMessageView__redeeming
GiftBadge.RedemptionState.REDEEMED -> R.string.GiftMessageView__redeemed
GiftBadge.RedemptionState.FAILED -> R.string.GiftMessageView__redeem
GiftBadge.RedemptionState.UNRECOGNIZED -> R.string.GiftMessageView__redeem
}
)
}
badgeView.setGiftBadge(giftBadge, glideRequests)
}
fun onGiftNotOpened() {
actionView.isClickable = false
}
fun onGiftOpened() {
actionView.isClickable = true
}
private fun stopAnimationIfNeeded() {
val icon = actionView.icon
if (icon is CircularProgressDrawable) {
icon.stop()
}
}
interface Callback {
fun onViewGiftBadgeClicked()
}
}

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