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
1616 changed files with 16088 additions and 61880 deletions

0
.github/stale.yml vendored
View File

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.LiveData
import androidx.lifecycle.ViewModel
import org.thoughtcrime.securesms.util.livedata.Store
import io.reactivex.rxjava3.disposables.CompositeDisposable
#end
#parse("File Header.java")
class ${NAME}ViewModel : ViewModel() {
private val store = Store(${NAME}State())
private val disposables = CompositeDisposable()
val state: LiveData<${NAME}State> = store.stateLiveData
override fun onCleared() {
disposables.clear()
}
}

View File

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

@@ -7,7 +7,6 @@ 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 {
@@ -63,31 +62,33 @@ ktlint {
version = "0.43.2"
}
def canonicalVersionCode = 1026
def canonicalVersionName = "5.34.0"
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',
]
@@ -118,48 +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'
}
defaultConfig {
versionCode canonicalVersionCode * postFixSize
versionName canonicalVersionName
@@ -187,33 +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", "\"2f79dc6c1599b71c70fc2d14f3ea2e3bc65134436eb87011c88845b137af673a\""
buildConfigField "String", "CDSH_CODE_HASH", "\"ec58c0d7561de8d5657f3a4b22a635eaa305204e9359dcc80a99dfd0c5f1cbf2\""
buildConfigField "String", "CDS_MRENCLAVE", "\"c98e00a4e3ff977a56afefe7362a27e4961e4f19e211febfbb19b897e6b80b15\""
buildConfigField "org.thoughtcrime.securesms.KbsEnclave", "KBS_ENCLAVE", "new org.thoughtcrime.securesms.KbsEnclave(\"0cedba03535b41b67729ce9924185f831d7767928a1d1689acb689bc079c375f\", " +
"\"187d2739d22be65e74b65f0055e74d31310e4267e5fac2b1246cc8beba81af39\", " +
"\"ee19f1965b1eefa3dc4204eb70c04f397755f771b8c1909d080c04dad2a6a9ba\")"
buildConfigField "org.thoughtcrime.securesms.KbsEnclave[]", "KBS_FALLBACKS", "new org.thoughtcrime.securesms.KbsEnclave[] {" +
"new org.thoughtcrime.securesms.KbsEnclave(\"fe7c1bfae98f9b073d220366ea31163ee82f6d04bead774f71ca8e5c40847bfe\", " +
"\"fe7c1bfae98f9b073d220366ea31163ee82f6d04bead774f71ca8e5c40847bfe\", " +
"\"a3baab19ef6ce6f34ab9ebb25ba722725ae44a8872dc0ff08ad6d83a9489de87\")" +
"}"
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\""
@@ -241,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',
@@ -256,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',
@@ -271,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
@@ -287,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 {
@@ -320,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'
@@ -341,18 +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_FALLBACKS", "new org.thoughtcrime.securesms.KbsEnclave[] {" +
"new org.thoughtcrime.securesms.KbsEnclave(\"823a3b2c037ff0cbe305cc48928cfcc97c9ed4a8ca6d49af6f7d6981fb60a4e9\", " +
"\"16b94ac6d2b7f7b9d72928f36d798dbb35ed32e7bb14c42b4301ad0344b46f29\", " +
"\"a3baab19ef6ce6f34ab9ebb25ba722725ae44a8872dc0ff08ad6d83a9489de87\")" +
"}"
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\""
@@ -366,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 {
@@ -393,6 +381,19 @@ android {
variant.setIgnore(true)
}
}
lintOptions {
checkReleaseBuilds false
abortOnError true
baseline file("lint-baseline.xml")
disable "LintError"
}
testOptions {
unitTests {
includeAndroidResources = true
}
}
}
dependencies {
@@ -518,22 +519,23 @@ 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")))

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,117 +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
fun givenStoryExistsAndMarkedNoReplies_getAllListsForContactSelectionUi_returnsStoryWithoutReplies() {
val id: DistributionListId? = distributionDatabase.createList("test", recipientList(1, 2, 3))
Assert.assertNotNull(id)
distributionDatabase.setAllowsReplies(id!!, false)
val records = distributionDatabase.getAllListsForContactSelectionUi(null, false)
Assert.assertFalse(records.first().allowsReplies)
}
@Test
fun givenStoryExists_getAllListsForContactSelectionUi_returnsStoryWithReplies() {
val id: DistributionListId? = distributionDatabase.createList("test", recipientList(1, 2, 3))
Assert.assertNotNull(id)
val records = distributionDatabase.getAllListsForContactSelectionUi(null, false)
Assert.assertTrue(records.first().allowsReplies)
}
@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

@@ -8,9 +8,6 @@ import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.signal.core.util.ThreadUtil
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.jobs.RecipientChangedNumberJob
import org.thoughtcrime.securesms.keyvalue.AccountValues
import org.thoughtcrime.securesms.keyvalue.KeyValueDataSet
import org.thoughtcrime.securesms.keyvalue.KeyValueStore
@@ -18,9 +15,9 @@ 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 org.whispersystems.signalservice.api.push.PNI
import java.util.Optional
import java.lang.IllegalArgumentException
import java.util.UUID
@RunWith(AndroidJUnit4::class)
@@ -28,16 +25,10 @@ class RecipientDatabaseTest {
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)
}
// ==============================================================
@@ -50,7 +41,7 @@ class RecipientDatabaseTest {
val recipientId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, null, true)
val recipient = Recipient.resolved(recipientId)
assertEquals(ACI_A, recipient.requireServiceId())
assertEquals(ACI_A, recipient.requireAci())
assertFalse(recipient.hasE164())
}
@@ -60,7 +51,7 @@ class RecipientDatabaseTest {
val recipientId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, null, false)
val recipient = Recipient.resolved(recipientId)
assertEquals(ACI_A, recipient.requireServiceId())
assertEquals(ACI_A, recipient.requireAci())
assertFalse(recipient.hasE164())
}
@@ -71,7 +62,7 @@ class RecipientDatabaseTest {
val recipient = Recipient.resolved(recipientId)
assertEquals(E164_A, recipient.requireE164())
assertFalse(recipient.hasServiceId())
assertFalse(recipient.hasAci())
}
/** If all you have is an E164, you can just store that, regardless of trust level. */
@@ -81,7 +72,7 @@ class RecipientDatabaseTest {
val recipient = Recipient.resolved(recipientId)
assertEquals(E164_A, recipient.requireE164())
assertFalse(recipient.hasServiceId())
assertFalse(recipient.hasAci())
}
/** With high trust, you can associate an ACI-e164 pair. */
@@ -90,7 +81,7 @@ class RecipientDatabaseTest {
val recipientId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
val recipient = Recipient.resolved(recipientId)
assertEquals(ACI_A, recipient.requireServiceId())
assertEquals(ACI_A, recipient.requireAci())
assertEquals(E164_A, recipient.requireE164())
}
@@ -100,7 +91,7 @@ class RecipientDatabaseTest {
val recipientId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, false)
val recipient = Recipient.resolved(recipientId)
assertEquals(ACI_A, recipient.requireServiceId())
assertEquals(ACI_A, recipient.requireAci())
assertFalse(recipient.hasE164())
}
@@ -111,26 +102,26 @@ class RecipientDatabaseTest {
/** 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 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.requireServiceId())
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.getOrInsertFromServiceId(ACI_A)
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.requireServiceId())
assertEquals(ACI_A, retrievedRecipient.requireAci())
assertFalse(retrievedRecipient.hasE164())
}
@@ -143,7 +134,7 @@ class RecipientDatabaseTest {
assertEquals(existingId, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
assertEquals(ACI_A, retrievedRecipient.requireAci())
assertEquals(E164_B, retrievedRecipient.requireE164())
}
@@ -156,7 +147,7 @@ class RecipientDatabaseTest {
assertEquals(existingId, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
assertEquals(ACI_A, retrievedRecipient.requireAci())
assertEquals(E164_A, retrievedRecipient.requireE164())
}
@@ -173,7 +164,7 @@ class RecipientDatabaseTest {
assertEquals(existingId, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
assertEquals(ACI_A, retrievedRecipient.requireAci())
assertEquals(E164_A, retrievedRecipient.requireE164())
}
@@ -186,30 +177,28 @@ class RecipientDatabaseTest {
assertNotEquals(existingId, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
assertEquals(ACI_A, retrievedRecipient.requireAci())
assertFalse(retrievedRecipient.hasE164())
val existingRecipient = Recipient.resolved(existingId)
assertEquals(E164_A, existingRecipient.requireE164())
assertFalse(existingRecipient.hasServiceId())
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)
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(ACI_B, retrievedRecipient.requireAci())
assertEquals(E164_A, retrievedRecipient.requireE164())
val existingRecipient = Recipient.resolved(existingId)
assertEquals(ACI_A, existingRecipient.requireServiceId())
assertEquals(ACI_A, existingRecipient.requireAci())
assertFalse(existingRecipient.hasE164())
}
@@ -222,11 +211,11 @@ class RecipientDatabaseTest {
assertNotEquals(existingId, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_B, retrievedRecipient.requireServiceId())
assertEquals(ACI_B, retrievedRecipient.requireAci())
assertFalse(retrievedRecipient.hasE164())
val existingRecipient = Recipient.resolved(existingId)
assertEquals(ACI_A, existingRecipient.requireServiceId())
assertEquals(ACI_A, existingRecipient.requireAci())
assertEquals(E164_A, existingRecipient.requireE164())
}
@@ -245,11 +234,11 @@ class RecipientDatabaseTest {
assertNotEquals(existingId, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_B, retrievedRecipient.requireServiceId())
assertEquals(ACI_B, retrievedRecipient.requireAci())
assertFalse(retrievedRecipient.hasE164())
val existingRecipient = Recipient.resolved(existingId)
assertEquals(ACI_A, existingRecipient.requireServiceId())
assertEquals(ACI_A, existingRecipient.requireAci())
assertEquals(E164_A, existingRecipient.requireE164())
}
@@ -266,80 +255,48 @@ class RecipientDatabaseTest {
assertEquals(existingId, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
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 changeNumberListener = ChangeNumberListener()
changeNumberListener.enqueue()
val existingAciId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, null, true)
val existingE164Id: RecipientId = recipientDatabase.getAndPossiblyMerge(null, E164_A, true)
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.requireServiceId())
assertEquals(ACI_A, retrievedRecipient.requireAci())
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 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.requireServiceId())
assertEquals(ACI_A, retrievedRecipient.requireAci())
assertFalse(retrievedRecipient.hasE164())
val existingE164Recipient = Recipient.resolved(existingE164Id)
assertEquals(E164_A, existingE164Recipient.requireE164())
assertFalse(existingE164Recipient.hasServiceId())
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 changeNumberListener = ChangeNumberListener()
changeNumberListener.enqueue()
val existingId1: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_B, true)
val existingId2: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_B, E164_A, true)
@@ -347,14 +304,12 @@ class RecipientDatabaseTest {
assertEquals(existingId1, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
assertEquals(ACI_A, retrievedRecipient.requireAci())
assertEquals(E164_A, retrievedRecipient.requireE164())
val existingRecipient2 = Recipient.resolved(existingId2)
assertEquals(ACI_B, existingRecipient2.requireServiceId())
assertEquals(ACI_B, existingRecipient2.requireAci())
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. */
@@ -367,11 +322,11 @@ class RecipientDatabaseTest {
assertEquals(existingId1, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
assertEquals(ACI_A, retrievedRecipient.requireAci())
assertEquals(E164_B, retrievedRecipient.requireE164())
val existingRecipient2 = Recipient.resolved(existingId2)
assertEquals(ACI_B, existingRecipient2.requireServiceId())
assertEquals(ACI_B, existingRecipient2.requireAci())
assertEquals(E164_A, existingRecipient2.requireE164())
}
@@ -388,7 +343,7 @@ class RecipientDatabaseTest {
assertEquals(existingId1, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
assertEquals(ACI_A, retrievedRecipient.requireAci())
assertEquals(E164_A, retrievedRecipient.requireE164())
assertFalse(recipientDatabase.getByE164(E164_B).isPresent)
@@ -413,71 +368,14 @@ class RecipientDatabaseTest {
assertEquals(existingId2, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
assertEquals(ACI_A, retrievedRecipient.requireAci())
assertFalse(retrievedRecipient.hasE164())
val recipientWithId1 = Recipient.resolved(existingId1)
assertEquals(ACI_B, recipientWithId1.requireServiceId())
assertEquals(ACI_B, recipientWithId1.requireAci())
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)
}
// ==============================================================
// Misc
// ==============================================================
@@ -502,18 +400,18 @@ class RecipientDatabaseTest {
@Test
fun createByUuidSanityCheck() {
// GIVEN one recipient
val recipientId: RecipientId = recipientDatabase.getOrInsertFromServiceId(ACI_A)
val recipientId: RecipientId = recipientDatabase.getOrInsertFromAci(ACI_A)
// WHEN I retrieve one by UUID
val possible: Optional<RecipientId> = recipientDatabase.getByServiceId(ACI_A)
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.serviceId.isPresent)
assertEquals(ACI_A, recipient.serviceId.get())
assertTrue(recipient.aci.isPresent)
assertEquals(ACI_A, recipient.aci.get())
}
@Test(expected = IllegalArgumentException::class)
@@ -528,31 +426,10 @@ class RecipientDatabaseTest {
}
}
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

@@ -9,6 +9,7 @@ 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
@@ -16,14 +17,11 @@ 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.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.groups.GroupId
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
@@ -33,10 +31,9 @@ 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.push.PNI
import org.whispersystems.signalservice.api.util.UuidUtil
import java.util.Optional
import java.util.UUID
@RunWith(AndroidJUnit4::class)
@@ -53,10 +50,6 @@ class RecipientDatabaseTest_merges {
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() {
@@ -71,19 +64,17 @@ class RecipientDatabaseTest_merges {
mentionDatabase = SignalDatabase.mentions
reactionDatabase = SignalDatabase.reactions
notificationProfileDatabase = SignalDatabase.notificationProfiles
distributionListDatabase = SignalDatabase.distributionLists
SignalStore.account().setAci(localAci)
SignalStore.account().setPni(localPni)
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.getOrInsertFromServiceId(ACI_A)
val recipientIdAci: RecipientId = recipientDatabase.getOrInsertFromAci(ACI_A)
val recipientIdE164: RecipientId = recipientDatabase.getOrInsertFromE164(E164_A)
val recipientIdAciB: RecipientId = recipientDatabase.getOrInsertFromServiceId(ACI_B)
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
@@ -108,7 +99,7 @@ class RecipientDatabaseTest_merges {
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())
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))
@@ -121,8 +112,6 @@ class RecipientDatabaseTest_merges {
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)!!
@@ -130,7 +119,7 @@ class RecipientDatabaseTest_merges {
// Recipient validation
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
assertEquals(ACI_A, retrievedRecipient.requireAci())
assertEquals(E164_A, retrievedRecipient.requireE164())
val existingE164Recipient = Recipient.resolved(recipientIdE164)
@@ -186,7 +175,7 @@ class RecipientDatabaseTest_merges {
assertNull(identityDatabase.getIdentityStoreRecord(E164_A))
// Session validation
assertNotNull(sessionDatabase.load(localAci, SignalProtocolAddress(ACI_A.toString(), 1)))
assertNotNull(sessionDatabase.load(SignalProtocolAddress(ACI_A.toString(), 1)))
// Reaction validation
val reactionsSms: List<ReactionRecord> = reactionDatabase.getReactions(MessageId(smsId1, false))
@@ -204,22 +193,24 @@ class RecipientDatabaseTest_merges {
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))
// Distribution List validation
val updatedList: DistributionListRecord = distributionListDatabase.getList(distributionListId)!!
assertThat("Distribution list should have updated $recipientIdE164 to $recipientIdAci", updatedList.members, Matchers.containsInAnyOrder(recipientIdAci, recipientIdAciB))
}
private val context: Application
get() = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as Application
private fun smsMessage(sender: RecipientId, time: Long = 0, body: String = "", groupId: Optional<GroupId> = Optional.empty()): IncomingTextMessage {
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.empty()): IncomingMediaMessage {
return IncomingMediaMessage(sender, groupId, body, time, time, time, emptyList(), 0, 0, false, false, true, Optional.empty())
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 {

View File

@@ -1,125 +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())
}
}

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.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.thoughtcrime.securesms.util.Hex
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

@@ -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"
@@ -310,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"
@@ -318,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" />
@@ -388,24 +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:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:theme="@style/TextSecure.DarkNoActionBar"
android:windowSoftInputMode="stateAlwaysHidden|adjustNothing" />
<activity android:name=".components.settings.app.changenumber.ChangeNumberLockActivity"
android:theme="@style/Signal.DayNight.NoActionBar"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
@@ -440,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"
@@ -648,13 +636,6 @@
<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>
@@ -757,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" />

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());
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,31 +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.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.RetrieveReleaseChannelJob;
import org.thoughtcrime.securesms.jobs.SubscriptionKeepAliveJob;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.logging.CustomSignalProtocolLogger;
@@ -67,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;
@@ -80,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;
@@ -94,6 +90,7 @@ 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(RetrieveReleaseChannelJob::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");
@@ -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

@@ -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 {
@@ -92,9 +92,6 @@ 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);
/** @return true if handled, false if you want to let the normal url handling continue */
boolean onUrlClicked(@NonNull String url);

View File

@@ -2,6 +2,7 @@ package org.thoughtcrime.securesms;
import androidx.annotation.NonNull;
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;

View File

@@ -76,11 +76,6 @@ 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(R.string.BlockUnblockDialog_blocked_people_wont_be_able_to_call_you_or_send_you_messages);
@@ -120,12 +115,6 @@ 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(R.string.BlockUnblockDialog_you_will_be_able_to_call_and_message_each_other);

View File

@@ -20,7 +20,6 @@ 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;
@@ -33,10 +32,10 @@ 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() {

View File

@@ -23,7 +23,6 @@ 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;
@@ -67,7 +66,6 @@ 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.sync.DirectoryHelper;
@@ -79,7 +77,6 @@ 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.TextSecurePreferences;
import org.thoughtcrime.securesms.util.UsernameUtil;
import org.thoughtcrime.securesms.util.ViewUtil;
@@ -87,11 +84,11 @@ import org.thoughtcrime.securesms.util.adapter.FixedViewsAdapter;
import org.thoughtcrime.securesms.util.adapter.RecyclerViewConcatenateAdapterStickyHeader;
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;
@@ -99,9 +96,10 @@ import java.util.function.Consumer;
* 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);
@@ -140,19 +138,18 @@ public final class ContactSelectionListFragment extends LoggingFragment
private AbstractContactsCursorLoaderFactoryProvider cursorFactoryProvider;
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) {
@@ -193,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
@@ -254,14 +243,11 @@ public final class ContactSelectionListFragment extends LoggingFragment
chipGroupScrollContainer = view.findViewById(R.id.chipGroupScrollContainer);
constraintLayout = view.findViewById(R.id.container);
shadowView = view.findViewById(R.id.toolbar_shadow);
headerActionView = view.findViewById(R.id.header_action);
toolbarShadowAnimationHelper = new ToolbarShadowAnimationHelper(shadowView);
final LinearLayoutManager layoutManager = new LinearLayoutManager(requireContext());
recyclerView.addOnScrollListener(toolbarShadowAnimationHelper);
recyclerView.setLayoutManager(layoutManager);
recyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
recyclerView.setItemAnimator(new DefaultItemAnimator() {
@Override
public boolean canReuseUpdatedViewHolder(@NonNull RecyclerView.ViewHolder viewHolder) {
@@ -299,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;
}
@@ -539,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() {
@@ -601,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();
@@ -659,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 -> {
@@ -756,7 +668,7 @@ public final class ContactSelectionListFragment extends LoggingFragment
private void addChipForSelectedContact(@NonNull SelectedContact selectedContact) {
SimpleTask.run(getViewLifecycleOwner().getLifecycle(),
() -> Recipient.resolved(selectedContact.getOrCreateRecipientId(requireContext())),
() -> Recipient.resolved(selectedContact.getOrCreateRecipientId(requireContext())),
resolved -> addChipForRecipient(resolved, selectedContact));
}
@@ -774,7 +686,7 @@ public final class ContactSelectionListFragment extends LoggingFragment
markContactUnselected(selectedContact);
if (onContactSelectedListener != null) {
onContactSelectedListener.onContactDeselected(Optional.of(recipient.getId()), recipient.getE164().orElse(null));
onContactSelectedListener.onContactDeselected(Optional.of(recipient.getId()), recipient.getE164().orNull());
}
});
@@ -834,11 +746,7 @@ public final class ContactSelectionListFragment extends LoggingFragment
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);
@@ -856,25 +764,19 @@ public final class ContactSelectionListFragment extends LoggingFragment
}
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);
}
@@ -882,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,13 +17,13 @@ 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.zkgroup.profiles.ProfileKey;
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;
@@ -36,6 +36,7 @@ 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

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

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,28 +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.Transformations;
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.ConversationListTabsState;
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.WindowUtil;
public class MainActivity extends PassphraseRequiredActivity implements VoiceNoteMediaControllerOwner {
@@ -35,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;
@@ -52,14 +42,9 @@ 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());
@@ -67,27 +52,12 @@ public class MainActivity extends PassphraseRequiredActivity implements VoiceNot
handleSignalMeIntent(getIntent());
CachedInflater.from(this).clear();
conversationListTabsViewModel = new ViewModelProvider(this, factory).get(ConversationListTabsViewModel.class);
Transformations.distinctUntilChanged(Transformations.map(conversationListTabsViewModel.getState(), ConversationListTabsState::getTab))
.observe(this, tab -> {
switch (tab) {
case CHATS:
getSupportFragmentManager().popBackStack();
break;
case STORIES:
navigator.goToStories();
break;
}
});
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);
}
@@ -112,8 +82,6 @@ public class MainActivity extends PassphraseRequiredActivity implements VoiceNot
if (SignalStore.misc().isOldDeviceTransferLocked()) {
OldDeviceTransferLockedDialog.show(getSupportFragmentManager());
}
updateTabVisibility();
}
@Override
@@ -131,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(getWindow(), ContextCompat.getColor(this, R.color.signal_background_secondary));
} else {
findViewById(R.id.conversation_list_tabs).setVisibility(View.GONE);
WindowUtil.setNavigationBarColor(getWindow(), ContextCompat.getColor(this, R.color.signal_background_primary));
navigator.goToChats();
}
}
public @NonNull MainNavigator getNavigator() {
return navigator;
}

View File

@@ -9,6 +9,7 @@ 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;
@@ -16,12 +17,9 @@ 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;
import org.thoughtcrime.securesms.stories.landing.StoriesLandingFragment;
public class MainNavigator {
public static final String STORIES_TAG = "STORIES";
public static final int REQUEST_CONFIG_CHANGES = 901;
private final MainActivity activity;
@@ -84,21 +82,6 @@ public class MainNavigator {
.commit();
}
public void goToStories() {
if (getFragmentManager().findFragmentByTag(STORIES_TAG) == null) {
getFragmentManager().beginTransaction()
.replace(R.id.fragment_container, new StoriesLandingFragment(), STORIES_TAG)
.addToBackStack(null)
.commit();
}
}
public void goToChats() {
if (getFragmentManager().findFragmentByTag(STORIES_TAG) != null) {
getFragmentManager().popBackStack();
}
}
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;
@@ -553,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();
@@ -721,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,
@@ -745,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,7 +21,6 @@ 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;
@@ -35,9 +34,9 @@ import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
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,7 +75,7 @@ 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 {
DirectoryHelper.refreshDirectoryFor(this, resolved, false);

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

@@ -9,9 +9,7 @@ import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.util.CharacterCalculator;
import org.thoughtcrime.securesms.util.CharacterCalculator.CharacterState;
import java.util.Optional;
import org.whispersystems.libsignal.util.guava.Optional;
public class TransportOption implements Parcelable {
@@ -20,8 +18,8 @@ public class TransportOption implements Parcelable {
TEXTSECURE
}
private final int drawable;
private final int backgroundColor;
private final int drawable;
private final int backgroundColor;
private final @NonNull String text;
private final @NonNull Type type;
private final @NonNull String composeHint;
@@ -37,7 +35,7 @@ public class TransportOption implements Parcelable {
@NonNull CharacterCalculator characterCalculator)
{
this(type, drawable, backgroundColor, text, composeHint, characterCalculator,
Optional.empty(), Optional.empty());
Optional.<CharSequence>absent(), Optional.<Integer>absent());
}
public TransportOption(@NonNull Type type,
@@ -66,8 +64,8 @@ public class TransportOption implements Parcelable {
in.readString(),
in.readString(),
CharacterCalculator.readFromParcel(in),
Optional.ofNullable(TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in)),
in.readInt() == 1 ? Optional.of(in.readInt()) : Optional.empty());
Optional.fromNullable(TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in)),
in.readInt() == 1 ? Optional.of(in.readInt()) : Optional.absent());
}
public @NonNull Type getType() {
@@ -125,7 +123,7 @@ public class TransportOption implements Parcelable {
dest.writeString(text);
dest.writeString(composeHint);
CharacterCalculator.writeToParcel(dest, characterCalculator);
TextUtils.writeToParcel(simName.orElse(null), dest, flags);
TextUtils.writeToParcel(simName.orNull(), dest, flags);
if (simSubscriptionId.isPresent()) {
dest.writeInt(1);

View File

@@ -14,14 +14,13 @@ 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.signalservice.api.util.OptionalUtil;
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 java.util.Optional;
import static org.thoughtcrime.securesms.TransportOption.Type;
@@ -34,8 +33,8 @@ public class TransportOptions {
private final List<TransportOption> enabledTransports;
private Type defaultTransportType = Type.SMS;
private Optional<Integer> defaultSubscriptionId = Optional.empty();
private Optional<TransportOption> selectedOption = Optional.empty();
private Optional<Integer> defaultSubscriptionId = Optional.absent();
private Optional<TransportOption> selectedOption = Optional.absent();
private final Optional<Integer> systemSubscriptionId;
@@ -55,7 +54,7 @@ public class TransportOptions {
setSelectedTransport(null);
} else {
this.defaultTransportType = Type.SMS;
this.defaultSubscriptionId = Optional.empty();
this.defaultSubscriptionId = Optional.absent();
notifyTransportChangeListeners();
}
@@ -82,7 +81,7 @@ public class TransportOptions {
}
public void setSelectedTransport(@Nullable TransportOption transportOption) {
this.selectedOption = Optional.ofNullable(transportOption);
this.selectedOption = Optional.fromNullable(transportOption);
notifyTransportChangeListeners();
}
@@ -94,7 +93,7 @@ public class TransportOptions {
if (selectedOption.isPresent()) return selectedOption.get();
if (defaultTransportType == Type.SMS) {
TransportOption transportOption = findEnabledSmsTransportOption(OptionalUtil.or(defaultSubscriptionId, systemSubscriptionId));
TransportOption transportOption = findEnabledSmsTransportOption(defaultSubscriptionId.or(systemSubscriptionId));
if (transportOption != null) {
return transportOption;
}
@@ -125,7 +124,7 @@ public class TransportOptions {
for (TransportOption transportOption : enabledTransports) {
if (transportOption.getType() == Type.SMS &&
subId == transportOption.getSimSubscriptionId().orElse(-1)) {
subId == transportOption.getSimSubscriptionId().or(-1)) {
return transportOption;
}
}
@@ -134,7 +133,7 @@ public class TransportOptions {
}
public void disableTransport(Type type) {
TransportOption selected = selectedOption.orElse(null);
TransportOption selected = selectedOption.orNull();
Iterator<TransportOption> iterator = enabledTransports.iterator();
while (iterator.hasNext()) {

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

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

@@ -1,90 +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.82f
avatar.scaleY = 0.82f
}
private fun hideStoryRing() {
storyRing.visible = false
avatar.scaleX = 1f
avatar.scaleY = 1f
}
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

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

View File

@@ -21,6 +21,7 @@ import org.signal.core.util.logging.Log;
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;
@@ -45,7 +46,6 @@ 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.CursorUtil;
import org.thoughtcrime.securesms.util.SetUtil;
import org.thoughtcrime.securesms.util.Stopwatch;
@@ -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));
@@ -441,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) {
@@ -456,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);
}
@@ -484,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());
}
@@ -504,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

@@ -32,7 +32,6 @@ 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;
@@ -69,17 +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"
};
public static void importFile(@NonNull Context context, @NonNull AttachmentSecret attachmentSecret,
@NonNull SQLiteDatabase db, @NonNull Uri uri, @NonNull String passphrase)
throws IOException
@@ -219,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.");
@@ -262,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()) {
@@ -283,10 +260,6 @@ 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);

View File

@@ -12,6 +12,7 @@ import org.thoughtcrime.securesms.mms.GlideApp
import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.ThemeUtil
import java.lang.IllegalArgumentException
class BadgeImageView @JvmOverloads constructor(
context: Context,

View File

@@ -1,51 +0,0 @@
package org.thoughtcrime.securesms.badges.self.expired
import androidx.core.content.ContextCompat
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
import org.thoughtcrime.securesms.components.settings.DSLSettingsBottomSheetFragment
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
import org.thoughtcrime.securesms.components.settings.configure
import org.thoughtcrime.securesms.components.settings.models.SplashImage
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.util.CommunicationActions
class CantProcessSubscriptionPaymentBottomSheetDialogFragment : DSLSettingsBottomSheetFragment() {
override fun bindAdapter(adapter: DSLSettingsAdapter) {
SplashImage.register(adapter)
adapter.submitList(getConfiguration().toMappingModelList())
}
private fun getConfiguration(): DSLConfiguration {
return configure {
customPref(SplashImage.Model(R.drawable.ic_card_process))
sectionHeaderPref(
title = DSLSettingsText.from(R.string.CantProcessSubscriptionPaymentBottomSheetDialogFragment__cant_process_subscription_payment, DSLSettingsText.CenterModifier)
)
textPref(
summary = DSLSettingsText.from(
requireContext().getString(R.string.CantProcessSubscriptionPaymentBottomSheetDialogFragment__were_having_trouble),
DSLSettingsText.LearnMoreModifier(ContextCompat.getColor(requireContext(), R.color.signal_accent_primary)) {
CommunicationActions.openBrowserLink(requireContext(), requireContext().getString(R.string.donation_decline_code_error_url))
},
DSLSettingsText.CenterModifier
)
)
primaryButton(
text = DSLSettingsText.from(android.R.string.ok)
) {
dismissAllowingStateLoss()
}
secondaryButtonNoOutline(
text = DSLSettingsText.from(R.string.CantProcessSubscriptionPaymentBottomSheetDialogFragment__dont_show_this_again)
) {
SignalStore.donationsValues().showCantProcessDialog = false
}
}
}
}

View File

@@ -10,7 +10,6 @@ import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
import org.thoughtcrime.securesms.components.settings.DSLSettingsBottomSheetFragment
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.UnexpectedSubscriptionCancellation
import org.thoughtcrime.securesms.components.settings.configure
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.util.BottomSheetUtil
@@ -28,13 +27,9 @@ class ExpiredBadgeBottomSheetDialogFragment : DSLSettingsBottomSheetFragment(
}
private fun getConfiguration(): DSLConfiguration {
val args = ExpiredBadgeBottomSheetDialogFragmentArgs.fromBundle(requireArguments())
val badge: Badge = args.badge
val cancellationReason: UnexpectedSubscriptionCancellation? = UnexpectedSubscriptionCancellation.fromStatus(args.cancelationReason)
val badge: Badge = ExpiredBadgeBottomSheetDialogFragmentArgs.fromBundle(requireArguments()).badge
val isLikelyASustainer = SignalStore.donationsValues().isLikelyASustainer()
val inactive = cancellationReason == UnexpectedSubscriptionCancellation.INACTIVE
return configure {
customPref(ExpiredBadge.Model(badge))
@@ -55,10 +50,8 @@ class ExpiredBadgeBottomSheetDialogFragment : DSLSettingsBottomSheetFragment(
DSLSettingsText.from(
if (badge.isBoost()) {
getString(R.string.ExpiredBadgeBottomSheetDialogFragment__your_boost_badge_has_expired)
} else if (inactive) {
getString(R.string.ExpiredBadgeBottomSheetDialogFragment__your_sustainer_subscription_was_automatically, badge.name)
} else {
getString(R.string.ExpiredBadgeBottomSheetDialogFragment__your_sustainer_subscription_was_canceled)
getString(R.string.ExpiredBadgeBottomSheetDialogFragment__your_sustainer, badge.name)
},
DSLSettingsText.CenterModifier
)
@@ -116,8 +109,8 @@ class ExpiredBadgeBottomSheetDialogFragment : DSLSettingsBottomSheetFragment(
companion object {
@JvmStatic
fun show(badge: Badge, cancellationReason: UnexpectedSubscriptionCancellation?, fragmentManager: FragmentManager) {
val args = ExpiredBadgeBottomSheetDialogFragmentArgs.Builder(badge, cancellationReason?.status).build()
fun show(badge: Badge, fragmentManager: FragmentManager) {
val args = ExpiredBadgeBottomSheetDialogFragmentArgs.Builder(badge).build()
val fragment = ExpiredBadgeBottomSheetDialogFragment()
fragment.arguments = args.toBundle()

View File

@@ -38,7 +38,7 @@ class BadgesOverviewFragment : DSLSettingsFragment(
override fun bindAdapter(adapter: DSLSettingsAdapter) {
Badge.register(adapter) { badge, _, isFaded ->
if (badge.isExpired() || isFaded) {
findNavController().safeNavigate(BadgesOverviewFragmentDirections.actionBadgeManageFragmentToExpiredBadgeDialog(badge, null))
findNavController().safeNavigate(BadgesOverviewFragmentDirections.actionBadgeManageFragmentToExpiredBadgeDialog(badge))
} else {
ViewBadgeBottomSheetDialogFragment.show(parentFragmentManager, Recipient.self().id, badge)
}

View File

@@ -17,7 +17,7 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.InternetConnectionObserver
import org.thoughtcrime.securesms.util.livedata.Store
import java.util.Optional
import org.whispersystems.libsignal.util.guava.Optional
private val TAG = Log.tag(BadgesOverviewViewModel::class.java)
@@ -54,13 +54,13 @@ class BadgesOverviewViewModel(
subscriptionsRepository.getSubscriptions()
) { active, all ->
if (!active.isActive && active.activeSubscription?.willCancelAtPeriodEnd() == true) {
Optional.ofNullable<String>(all.firstOrNull { it.level == active.activeSubscription?.level }?.badge?.id)
Optional.fromNullable<String>(all.firstOrNull { it.level == active.activeSubscription?.level }?.badge?.id)
} else {
Optional.empty()
Optional.absent()
}
}.subscribeBy(
onSuccess = { badgeId ->
store.update { it.copy(fadedBadgeId = badgeId.orElse(null)) }
store.update { it.copy(fadedBadgeId = badgeId.orNull()) }
},
onError = { throwable ->
Log.w(TAG, "Could not retrieve data from server", throwable)

View File

@@ -4,6 +4,7 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.kotlin.plusAssign
import org.thoughtcrime.securesms.badges.BadgeRepository
import org.thoughtcrime.securesms.badges.models.Badge
import org.thoughtcrime.securesms.recipients.Recipient

View File

@@ -24,8 +24,8 @@ import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.whispersystems.libsignal.util.guava.Optional;
import java.util.Optional;
import java.util.function.Consumer;
public class BlockedUsersActivity extends PassphraseRequiredActivity implements BlockedUsersFragment.Listener, ContactSelectionListFragment.OnContactSelectedListener {
@@ -87,8 +87,8 @@ public class BlockedUsersActivity extends PassphraseRequiredActivity implements
}
@Override
public void onBeforeContactSelected(@NonNull Optional<RecipientId> recipientId, String number, @NonNull Consumer<Boolean> callback) {
final String displayName = recipientId.map(id -> Recipient.resolved(id).getDisplayName(this)).orElse(number);
public void onBeforeContactSelected(Optional<RecipientId> recipientId, String number, Consumer<Boolean> callback) {
final String displayName = recipientId.transform(id -> Recipient.resolved(id).getDisplayName(this)).or(number);
AlertDialog confirmationDialog = new MaterialAlertDialogBuilder(this)
.setTitle(R.string.BlockedUsersActivity__block_user)
@@ -116,7 +116,7 @@ public class BlockedUsersActivity extends PassphraseRequiredActivity implements
}
@Override
public void onContactDeselected(@NonNull Optional<RecipientId> recipientId, String number) {
public void onContactDeselected(Optional<RecipientId> recipientId, String number) {
}

View File

@@ -15,7 +15,6 @@ import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.AvatarImageView;
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.whispersystems.signalservice.api.util.OptionalUtil;
import java.util.Objects;
@@ -65,9 +64,7 @@ final class BlockedUsersAdapter extends ListAdapter<Recipient, BlockedUsersAdapt
displayName.setText(recipient.getDisplayName(itemView.getContext()));
if (recipient.hasAUserSetDisplayName(itemView.getContext())) {
String identifier = OptionalUtil.or(recipient.getE164().map(PhoneNumberFormatter::prettyPrint),
recipient.getUsername())
.orElse(null);
String identifier = recipient.getE164().transform(PhoneNumberFormatter::prettyPrint).or(recipient.getUsername()).orNull();
if (identifier != null) {
numberOrUsername.setText(identifier);

View File

@@ -1,6 +1,9 @@
package org.thoughtcrime.securesms.blocked;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.graphics.Color;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
@@ -12,7 +15,6 @@ import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProviders;
import androidx.recyclerview.widget.RecyclerView;
import org.thoughtcrime.securesms.BlockUnblockDialog;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.recipients.Recipient;
@@ -72,9 +74,24 @@ public class BlockedUsersFragment extends Fragment {
}
private void handleRecipientClicked(@NonNull Recipient recipient) {
BlockUnblockDialog.showUnblockFor(requireContext(), getViewLifecycleOwner().getLifecycle(), recipient, () -> {
viewModel.unblock(recipient.getId());
AlertDialog confirmationDialog = new AlertDialog.Builder(requireContext())
.setTitle(R.string.BlockedUsersActivity__unblock_user)
.setMessage(getString(R.string.BlockedUsersActivity__do_you_want_to_unblock_s, recipient.getDisplayName(requireContext())))
.setPositiveButton(R.string.BlockedUsersActivity__unblock, (dialog, which) -> {
viewModel.unblock(recipient.getId());
dialog.dismiss();
})
.setNegativeButton(android.R.string.cancel, (dialog, which) -> {
dialog.dismiss();
})
.setCancelable(true)
.create();
confirmationDialog.setOnShowListener(dialog -> {
confirmationDialog.getButton(DialogInterface.BUTTON_POSITIVE).setTextColor(Color.RED);
});
confirmationDialog.show();
}
interface Listener {

View File

@@ -95,7 +95,7 @@ public final class AvatarImageView extends AppCompatImageView {
initialize(context, attrs);
}
public void initialize(@NonNull Context context, @Nullable AttributeSet attrs) {
private void initialize(@NonNull Context context, @Nullable AttributeSet attrs) {
setScaleType(ScaleType.CENTER_CROP);
if (attrs != null) {

View File

@@ -41,10 +41,10 @@ import org.thoughtcrime.securesms.util.SignalLocalMetrics;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.dualsim.SubscriptionInfoCompat;
import org.thoughtcrime.securesms.util.dualsim.SubscriptionManagerCompat;
import org.whispersystems.libsignal.util.guava.Optional;
import java.util.Locale;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
public class ConversationItemFooter extends ConstraintLayout {
@@ -246,7 +246,7 @@ public class ConversationItemFooter extends ConstraintLayout {
});
if (isOutgoing) {
dateView.setMaxWidth(ViewUtil.dpToPx(32));
dateView.setMaxWidth(ViewUtil.dpToPx(28));
} else {
ConstraintSet constraintSet = new ConstraintSet();
constraintSet.clone(this);

View File

@@ -11,11 +11,15 @@ import androidx.annotation.Nullable;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.core.content.ContextCompat;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.badges.BadgeImageView;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.whispersystems.libsignal.util.Pair;
import java.util.LinkedList;
import java.util.List;
public class ConversationTypingView extends ConstraintLayout {

View File

@@ -7,9 +7,11 @@ import android.graphics.Path;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.RectF;
import android.graphics.drawable.shapes.RoundRectShape;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
public class CornerMask {

View File

@@ -27,7 +27,6 @@ import org.thoughtcrime.securesms.events.PartProgressEvent;
import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.mms.SlideClickListener;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.signalservice.api.util.OptionalUtil;
public class DocumentView extends FrameLayout {
@@ -106,11 +105,11 @@ public class DocumentView extends FrameLayout {
this.documentSlide = documentSlide;
this.fileName.setText(OptionalUtil.or(documentSlide.getFileName(),
documentSlide.getCaption())
.orElse(getContext().getString(R.string.DocumentView_unnamed_file)));
this.fileName.setText(documentSlide.getFileName()
.or(documentSlide.getCaption())
.or(getContext().getString(R.string.DocumentView_unnamed_file)));
this.fileSize.setText(Util.getPrettyFileSize(documentSlide.getFileSize()));
this.document.setText(documentSlide.getFileType(getContext()).orElse("").toLowerCase());
this.document.setText(documentSlide.getFileType(getContext()).or("").toLowerCase());
this.setOnClickListener(new OpenClickedListener(documentSlide));
}

View File

@@ -2,9 +2,8 @@ package org.thoughtcrime.securesms.components
import android.app.Dialog
import android.os.Bundle
import android.view.ContextThemeWrapper
import android.view.View
import androidx.annotation.StyleRes
import androidx.core.content.ContextCompat
import androidx.core.view.ViewCompat
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
@@ -13,7 +12,6 @@ import com.google.android.material.shape.CornerFamily
import com.google.android.material.shape.MaterialShapeDrawable
import com.google.android.material.shape.ShapeAppearanceModel
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.util.ThemeUtil
import org.thoughtcrime.securesms.util.ViewUtil
/**
@@ -23,12 +21,9 @@ abstract class FixedRoundedCornerBottomSheetDialogFragment : BottomSheetDialogFr
protected open val peekHeightPercentage: Float = 0.5f
@StyleRes
protected open val themeResId: Int = R.style.Widget_Signal_FixedRoundedCorners
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStyle(STYLE_NORMAL, themeResId)
setStyle(STYLE_NORMAL, R.style.Widget_Signal_FixedRoundedCorners)
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
@@ -43,8 +38,7 @@ abstract class FixedRoundedCornerBottomSheetDialogFragment : BottomSheetDialogFr
val dialogBackground = MaterialShapeDrawable(shapeAppearanceModel)
val bottomSheetStyle = ThemeUtil.getThemedResourceId(ContextThemeWrapper(requireContext(), themeResId), R.attr.bottomSheetStyle)
dialogBackground.setTint(ThemeUtil.getThemedColor(ContextThemeWrapper(requireContext(), bottomSheetStyle), R.attr.backgroundTint))
dialogBackground.setTint(ContextCompat.getColor(requireContext(), R.color.signal_background_dialog))
dialog.behavior.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() {
override fun onStateChanged(bottomSheet: View, newState: Int) {

View File

@@ -1,35 +0,0 @@
package org.thoughtcrime.securesms.components
import android.os.Bundle
import androidx.fragment.app.Fragment
import org.thoughtcrime.securesms.PassphraseRequiredActivity
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme
import org.thoughtcrime.securesms.util.DynamicTheme
/**
* Activity that wraps a given fragment
*/
abstract class FragmentWrapperActivity : PassphraseRequiredActivity() {
protected open val dynamicTheme: DynamicTheme = DynamicNoActionBarTheme()
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
super.onCreate(savedInstanceState, ready)
setContentView(R.layout.fragment_container)
dynamicTheme.onCreate(this)
if (savedInstanceState == null) {
supportFragmentManager.beginTransaction()
.replace(R.id.fragment_container, getFragment())
.commit()
}
}
abstract fun getFragment(): Fragment
override fun onResume() {
super.onResume()
dynamicTheme.onResume(this)
}
}

View File

@@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.components;
import android.content.Context;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.text.Spannable;
import android.text.SpannableString;
@@ -17,7 +18,6 @@ import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.emoji.SimpleEmojiTextView;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.ContextUtil;
import org.thoughtcrime.securesms.util.SpanUtil;
import org.thoughtcrime.securesms.util.ViewUtil;
@@ -44,7 +44,7 @@ public class FromTextView extends SimpleEmojiTextView {
}
public void setText(Recipient recipient, boolean read, @Nullable String suffix) {
setText(recipient, recipient.getDisplayNameOrUsername(getContext()), read, suffix);
setText(recipient, recipient.getDisplayName(getContext()), read, suffix);
}
public void setText(Recipient recipient, @Nullable CharSequence fromString, boolean read, @Nullable String suffix) {
@@ -62,19 +62,11 @@ public class FromTextView extends SimpleEmojiTextView {
builder.append(suffix);
}
if (recipient.isReleaseNotes()) {
Drawable official = ContextUtil.requireDrawable(getContext(), R.drawable.ic_official_20);
official.setBounds(0, 0, ViewUtil.dpToPx(20), ViewUtil.dpToPx(20));
builder.append(" ")
.append(SpanUtil.buildCenteredImageSpan(official));
}
setText(builder);
if (recipient.isBlocked()) setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_block_grey600_18dp, 0, 0, 0);
else if (recipient.isMuted()) setCompoundDrawablesRelativeWithIntrinsicBounds(getMuted(), null, null, null);
else setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0);
else setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0);
}
private Drawable getMuted() {

View File

@@ -13,6 +13,7 @@ import androidx.appcompat.widget.Toolbar;
import androidx.fragment.app.DialogFragment;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.ThemeUtil;
/**
* Base dialog fragment for rendering as a full screen dialog with animation
@@ -34,11 +35,7 @@ public abstract class FullScreenDialogFragment extends DialogFragment {
View view = inflater.inflate(R.layout.full_screen_dialog_fragment, container, false);
inflater.inflate(getDialogLayoutResource(), view.findViewById(R.id.full_screen_dialog_content), true);
toolbar = view.findViewById(R.id.full_screen_dialog_toolbar);
if (getTitle() != -1) {
toolbar.setTitle(getTitle());
}
toolbar.setTitle(getTitle());
toolbar.setNavigationOnClickListener(v -> onNavigateUp());
return view;
}

View File

@@ -1,8 +1,6 @@
package org.thoughtcrime.securesms.components;
import android.content.Context;
import android.os.Build;
import android.text.InputType;
import android.util.AttributeSet;
import android.widget.EditText;
@@ -14,7 +12,6 @@ import org.thoughtcrime.securesms.util.ServiceUtil;
public class InputAwareLayout extends KeyboardAwareLinearLayout implements OnKeyboardShownListener {
private InputView current;
private int previousInputType = InputType.TYPE_NULL;
public InputAwareLayout(Context context) {
this(context, null);
@@ -47,8 +44,6 @@ public class InputAwareLayout extends KeyboardAwareLinearLayout implements OnKey
input.show(getKeyboardHeight(), current != null);
current = input;
}
setShowSoftInputOnFocusCompat(imeTarget, false);
}
public InputView getCurrentInput() {
@@ -58,8 +53,6 @@ public class InputAwareLayout extends KeyboardAwareLinearLayout implements OnKey
public void hideCurrentInput(EditText imeTarget) {
if (isKeyboardOpen()) hideSoftkey(imeTarget, null);
else hideAttachedInput(false);
setShowSoftInputOnFocusCompat(imeTarget, true);
}
public void hideAttachedInput(boolean instant) {
@@ -72,13 +65,6 @@ public class InputAwareLayout extends KeyboardAwareLinearLayout implements OnKey
}
public void showSoftkey(final EditText inputTarget) {
showSoftkey(inputTarget, false);
}
public void showSoftkey(final EditText inputTarget, boolean force) {
if (!force && isInputOpen()) return;
setShowSoftInputOnFocusCompat(inputTarget, true);
postOnKeyboardOpen(new Runnable() {
@Override public void run() {
hideAttachedInput(true);
@@ -99,20 +85,6 @@ public class InputAwareLayout extends KeyboardAwareLinearLayout implements OnKey
.hideSoftInputFromWindow(inputTarget.getWindowToken(), 0);
}
private void setShowSoftInputOnFocusCompat(EditText imeTarget, boolean show) {
if (Build.VERSION.SDK_INT >= 21) {
imeTarget.setShowSoftInputOnFocus(show);
} else if (show) {
if (previousInputType != InputType.TYPE_NULL) {
imeTarget.setInputType(previousInputType);
}
} else {
previousInputType = imeTarget.getInputType();
imeTarget.setRawInputType(InputType.TYPE_CLASS_TEXT);
imeTarget.setTextIsSelectable(true);
}
}
public interface InputView {
void show(int height, boolean immediate);
void hide(boolean immediate);

View File

@@ -53,11 +53,11 @@ import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.concurrent.AssertedSuccessListener;
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
import org.thoughtcrime.securesms.util.concurrent.SettableFuture;
import org.whispersystems.libsignal.util.guava.Optional;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
public class InputPanel extends LinearLayout
@@ -79,8 +79,7 @@ public class InputPanel extends LinearLayout
private ComposeText composeText;
private View quickCameraToggle;
private View quickAudioToggle;
private AnimatingToggle buttonToggle;
private SendButton sendButton;
private View buttonToggle;
private View recordingContainer;
private View recordLockCancel;
private ViewGroup composeContainer;
@@ -128,7 +127,6 @@ public class InputPanel extends LinearLayout
this.quickCameraToggle = findViewById(R.id.quick_camera_toggle);
this.quickAudioToggle = findViewById(R.id.quick_audio_toggle);
this.buttonToggle = findViewById(R.id.button_toggle);
this.sendButton = findViewById(R.id.send_button);
this.recordingContainer = findViewById(R.id.recording_container);
this.recordLockCancel = findViewById(R.id.record_cancel);
this.voiceNoteDraftView = findViewById(R.id.voice_note_draft_view);
@@ -187,13 +185,7 @@ public class InputPanel extends LinearLayout
: 0;
this.quoteView.setVisibility(VISIBLE);
int maxWidth = composeContainer.getWidth();
if (quoteView.getLayoutParams() instanceof MarginLayoutParams) {
MarginLayoutParams layoutParams = (MarginLayoutParams) quoteView.getLayoutParams();
maxWidth -= layoutParams.leftMargin + layoutParams.rightMargin;
}
this.quoteView.measure(MeasureSpec.makeMeasureSpec(maxWidth, MeasureSpec.AT_MOST), 0);
this.quoteView.measure(0, 0);
if (quoteAnimator != null) {
quoteAnimator.cancel();
@@ -258,7 +250,7 @@ public class InputPanel extends LinearLayout
if (quoteView.getQuoteId() > 0 && quoteView.getVisibility() == View.VISIBLE) {
return Optional.of(new QuoteModel(quoteView.getQuoteId(), quoteView.getAuthor().getId(), quoteView.getBody().toString(), false, quoteView.getAttachments(), quoteView.getMentions()));
} else {
return Optional.empty();
return Optional.absent();
}
}
@@ -362,13 +354,13 @@ public class InputPanel extends LinearLayout
slideToCancel.display();
if (emojiVisible) {
fadeOut(mediaKeyboard);
ViewUtil.fadeOut(mediaKeyboard, FADE_TIME, View.INVISIBLE);
}
fadeOut(composeText);
fadeOut(quickCameraToggle);
fadeOut(quickAudioToggle);
fadeOut(buttonToggle);
ViewUtil.fadeOut(composeText, FADE_TIME, View.INVISIBLE);
ViewUtil.fadeOut(quickCameraToggle, FADE_TIME, View.INVISIBLE);
ViewUtil.fadeOut(quickAudioToggle, FADE_TIME, View.INVISIBLE);
buttonToggle.animate().alpha(0).setDuration(FADE_TIME).start();
}
@Override
@@ -409,7 +401,7 @@ public class InputPanel extends LinearLayout
public void onRecordLocked() {
slideToCancel.hide();
recordLockCancel.setVisibility(View.VISIBLE);
fadeIn(buttonToggle);
buttonToggle.animate().alpha(1).setDuration(FADE_TIME).start();
if (listener != null) listener.onRecorderLocked();
}
@@ -483,7 +475,6 @@ public class InputPanel extends LinearLayout
voiceNoteDraftView.setDraft(voiceNoteDraft);
voiceNoteDraftView.setVisibility(VISIBLE);
hideNormalComposeViews();
buttonToggle.displayQuick(sendButton);
} else {
voiceNoteDraftView.clearDraft();
ViewUtil.fadeOut(voiceNoteDraftView, FADE_TIME);
@@ -497,33 +488,36 @@ public class InputPanel extends LinearLayout
private void hideNormalComposeViews() {
if (emojiVisible) {
mediaKeyboard.animate().cancel();
mediaKeyboard.setAlpha(0f);
Animation animation = mediaKeyboard.getAnimation();
if (animation != null) {
animation.cancel();
}
mediaKeyboard.setVisibility(View.INVISIBLE);
}
for (View view : Arrays.asList(composeText, quickCameraToggle, quickAudioToggle)) {
view.animate().cancel();
view.setAlpha(0f);
for (Animation animation : Arrays.asList(composeText.getAnimation(), quickCameraToggle.getAnimation(), quickAudioToggle.getAnimation())) {
if (animation != null) {
animation.cancel();
}
}
buttonToggle.animate().cancel();
composeText.setVisibility(View.INVISIBLE);
quickCameraToggle.setVisibility(View.INVISIBLE);
quickAudioToggle.setVisibility(View.INVISIBLE);
}
private void fadeInNormalComposeViews() {
if (emojiVisible) {
fadeIn(mediaKeyboard);
ViewUtil.fadeIn(mediaKeyboard, FADE_TIME);
}
fadeIn(composeText);
fadeIn(quickCameraToggle);
fadeIn(quickAudioToggle);
fadeIn(buttonToggle);
}
private void fadeIn(@NonNull View v) {
v.animate().alpha(1).setDuration(FADE_TIME).start();
}
private void fadeOut(@NonNull View v) {
v.animate().alpha(0).setDuration(FADE_TIME).start();
ViewUtil.fadeIn(composeText, FADE_TIME);
ViewUtil.fadeIn(quickCameraToggle, FADE_TIME);
ViewUtil.fadeIn(quickAudioToggle, FADE_TIME);
buttonToggle.animate().alpha(1).setDuration(FADE_TIME).start();
}
private void updateVisibility() {

View File

@@ -13,6 +13,7 @@ import androidx.annotation.Nullable;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.constraintlayout.widget.Guideline;
import org.signal.glide.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.ViewUtil;

View File

@@ -20,8 +20,6 @@ abstract class KeyboardEntryDialogFragment(@LayoutRes contentLayoutId: Int) :
private var hasShown = false
protected open val withDim: Boolean = false
override fun onCreate(savedInstanceState: Bundle?) {
setStyle(STYLE_NORMAL, R.style.Theme_Signal_RoundedBottomSheet)
super.onCreate(savedInstanceState)
@@ -31,10 +29,7 @@ abstract class KeyboardEntryDialogFragment(@LayoutRes contentLayoutId: Int) :
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val dialog = super.onCreateDialog(savedInstanceState)
if (!withDim) {
dialog.window?.setDimAmount(0f)
}
dialog.window?.setDimAmount(0f)
dialog.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
return dialog

View File

@@ -22,13 +22,13 @@ import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.mms.ImageSlide;
import org.thoughtcrime.securesms.mms.SlidesClickedListener;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Locale;
import okhttp3.HttpUrl;
import org.thoughtcrime.securesms.util.ViewUtil;
/**
* The view shown in the compose box or conversation that represents the state of the link preview.

View File

@@ -19,6 +19,7 @@ import android.widget.ImageView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.view.ViewCompat;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.permissions.Permissions;

View File

@@ -29,17 +29,13 @@ public class OutlinedThumbnailView extends ThumbnailView {
cornerMask = new CornerMask(this);
outliner = new Outliner();
int defaultOutlinerColor = ContextCompat.getColor(getContext(), R.color.signal_inverse_transparent_20);
outliner.setColor(defaultOutlinerColor);
outliner.setColor(ContextCompat.getColor(getContext(), R.color.signal_inverse_transparent_20));
int radius = 0;
if (attrs != null) {
TypedArray typedArray = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.OutlinedThumbnailView, 0, 0);
radius = typedArray.getDimensionPixelOffset(R.styleable.OutlinedThumbnailView_otv_cornerRadius, 0);
outliner.setStrokeWidth(typedArray.getDimensionPixelSize(R.styleable.OutlinedThumbnailView_otv_strokeWidth, 1));
outliner.setColor(typedArray.getColor(R.styleable.OutlinedThumbnailView_otv_strokeColor, defaultOutlinerColor));
}
setRadius(radius);

View File

@@ -19,6 +19,7 @@ import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.core.content.ContextCompat;
import com.annimon.stream.Stream;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import org.signal.core.util.logging.Log;
@@ -27,7 +28,6 @@ import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.components.mention.MentionAnnotation;
import org.thoughtcrime.securesms.conversation.colors.ChatColors;
import org.thoughtcrime.securesms.database.model.Mention;
import org.thoughtcrime.securesms.database.model.databaseprotos.StoryTextPost;
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.mms.Slide;
@@ -35,7 +35,6 @@ import org.thoughtcrime.securesms.mms.SlideDeck;
import org.thoughtcrime.securesms.recipients.LiveRecipient;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientForeverObserver;
import org.thoughtcrime.securesms.stories.StoryTextPostModel;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.Projection;
import org.thoughtcrime.securesms.util.ThemeUtil;
@@ -46,30 +45,9 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
private static final String TAG = Log.tag(QuoteView.class);
public enum MessageType {
// These codes must match the values for the QuoteView_message_type XML attribute.
PREVIEW(0),
OUTGOING(1),
INCOMING(2),
STORY_REPLY(3),
STORY_REPLY_PREVIEW(4);
private final int code;
MessageType(int code) {
this.code = code;
}
private static @NonNull MessageType fromCode(int code) {
for (MessageType value : values()) {
if (value.code == code) {
return value;
}
}
throw new IllegalArgumentException("Unsupported code " + code);
}
}
private static final int MESSAGE_TYPE_PREVIEW = 0;
private static final int MESSAGE_TYPE_OUTGOING = 1;
private static final int MESSAGE_TYPE_INCOMING = 2;
private ViewGroup mainView;
private ViewGroup footerView;
@@ -88,13 +66,11 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
private TextView mediaDescriptionText;
private TextView missingLinkText;
private SlideDeck attachments;
private MessageType messageType;
private int messageType;
private int largeCornerRadius;
private int smallCornerRadius;
private CornerMask cornerMask;
private int thumbHeight;
private int thumbWidth;
public QuoteView(Context context) {
super(context);
@@ -136,24 +112,29 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
this.smallCornerRadius = getResources().getDimensionPixelSize(R.dimen.quote_corner_radius_bottom);
cornerMask = new CornerMask(this);
cornerMask.setRadii(largeCornerRadius, largeCornerRadius, smallCornerRadius, smallCornerRadius);
if (attrs != null) {
TypedArray typedArray = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.QuoteView, 0, 0);
int primaryColor = typedArray.getColor(R.styleable.QuoteView_quote_colorPrimary, Color.BLACK);
int secondaryColor = typedArray.getColor(R.styleable.QuoteView_quote_colorSecondary, Color.BLACK);
messageType = MessageType.fromCode(typedArray.getInt(R.styleable.QuoteView_message_type, 0));
messageType = typedArray.getInt(R.styleable.QuoteView_message_type, 0);
typedArray.recycle();
dismissView.setVisibility(messageType == MessageType.PREVIEW ? VISIBLE : GONE);
dismissView.setVisibility(messageType == MESSAGE_TYPE_PREVIEW ? VISIBLE : GONE);
authorView.setTextColor(primaryColor);
bodyView.setTextColor(primaryColor);
attachmentNameView.setTextColor(primaryColor);
mediaDescriptionText.setTextColor(secondaryColor);
missingLinkText.setTextColor(primaryColor);
}
setMessageType(messageType);
if (messageType == MESSAGE_TYPE_PREVIEW) {
int radius = getResources().getDimensionPixelOffset(R.dimen.quote_corner_radius_preview);
cornerMask.setTopLeftRadius(radius);
cornerMask.setTopRightRadius(radius);
}
}
dismissView.setOnClickListener(view -> setVisibility(GONE));
}
@@ -170,30 +151,6 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
if (author != null) author.removeForeverObserver(this);
}
public void setMessageType(@NonNull MessageType messageType) {
this.messageType = messageType;
cornerMask.setRadii(largeCornerRadius, largeCornerRadius, smallCornerRadius, smallCornerRadius);
thumbWidth = thumbHeight = getResources().getDimensionPixelSize(R.dimen.quote_thumb_size);
if (messageType == MessageType.PREVIEW) {
int radius = getResources().getDimensionPixelOffset(R.dimen.quote_corner_radius_preview);
cornerMask.setTopLeftRadius(radius);
cornerMask.setTopRightRadius(radius);
} else if (isStoryReply()) {
thumbWidth = getResources().getDimensionPixelOffset(R.dimen.quote_story_thumb_width);
thumbHeight = getResources().getDimensionPixelOffset(R.dimen.quote_story_thumb_height);
}
mainView.setMinimumHeight(thumbHeight);
ViewGroup.LayoutParams params = thumbnailView.getLayoutParams();
params.height = thumbHeight;
params.width = thumbWidth;
thumbnailView.setLayoutParams(params);
}
public void setQuote(GlideRequests glideRequests,
long id,
@NonNull Recipient author,
@@ -212,10 +169,10 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
this.author.observeForever(this);
setQuoteAuthor(author);
setQuoteText(body, attachments);
setQuoteAttachment(glideRequests, body, attachments);
setQuoteAttachment(glideRequests, attachments);
setQuoteMissingFooter(originalMissing);
if (Build.VERSION.SDK_INT < 21 && messageType == MessageType.INCOMING && chatColors != null) {
if (Build.VERSION.SDK_INT < 21 && messageType == MESSAGE_TYPE_INCOMING && chatColors != null) {
this.setBackgroundColor(chatColors.asSingleColor());
} else {
this.setBackground(null);
@@ -251,41 +208,20 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
}
private void setQuoteAuthor(@NonNull Recipient author) {
boolean outgoing = messageType != MessageType.INCOMING;
boolean preview = messageType == MessageType.PREVIEW || messageType == MessageType.STORY_REPLY_PREVIEW;
boolean outgoing = messageType != MESSAGE_TYPE_INCOMING;
boolean preview = messageType == MESSAGE_TYPE_PREVIEW;
if (isStoryReply()) {
authorView.setText(author.isSelf() ? getContext().getString(R.string.QuoteView_your_story)
: getContext().getString(R.string.QuoteView_s_story, author.getDisplayName(getContext())));
} else {
authorView.setText(author.isSelf() ? getContext().getString(R.string.QuoteView_you)
: author.getDisplayName(getContext()));
}
authorView.setText(author.isSelf() ? getContext().getString(R.string.QuoteView_you)
: author.getDisplayName(getContext()));
quoteBarView.setBackgroundColor(ContextCompat.getColor(getContext(), outgoing ? R.color.core_white : android.R.color.transparent));
mainView.setBackgroundColor(ContextCompat.getColor(getContext(), preview ? R.color.quote_preview_background : R.color.quote_view_background));
}
private boolean isStoryReply() {
return messageType == MessageType.STORY_REPLY || messageType == MessageType.STORY_REPLY_PREVIEW;
}
private void setQuoteText(@Nullable CharSequence body, @NonNull SlideDeck attachments) {
boolean isTextStory = !attachments.containsMediaSlide() && isStoryReply();
if (!TextUtils.isEmpty(body) || !attachments.containsMediaSlide()) {
if (isTextStory && body != null) {
try {
bodyView.setText(StoryTextPostModel.parseFrom(body.toString(), id, author.getId()).getText());
} catch (Exception e) {
Log.w(TAG, "Could not parse body of text post.", e);
bodyView.setText("");
}
} else {
bodyView.setText(body == null ? "" : body);
}
bodyView.setVisibility(VISIBLE);
bodyView.setText(body == null ? "" : body);
mediaDescriptionText.setVisibility(GONE);
return;
}
@@ -324,20 +260,7 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
}
}
private void setQuoteAttachment(@NonNull GlideRequests glideRequests, @NonNull CharSequence body, @NonNull SlideDeck slideDeck) {
if (!attachments.containsMediaSlide() && isStoryReply()) {
StoryTextPostModel model = StoryTextPostModel.parseFrom(body.toString(), id, author.getId());
attachmentVideoOverlayView.setVisibility(GONE);
attachmentContainerView.setVisibility(GONE);
thumbnailView.setVisibility(VISIBLE);
glideRequests.load(model)
.centerCrop()
.override(thumbWidth, thumbHeight)
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.into(thumbnailView);
return;
}
private void setQuoteAttachment(@NonNull GlideRequests glideRequests, @NonNull SlideDeck slideDeck) {
Slide imageVideoSlide = slideDeck.getSlides().stream().filter(s -> s.hasImage() || s.hasVideo() || s.hasSticker()).findFirst().orElse(null);
Slide documentSlide = slideDeck.getSlides().stream().filter(Slide::hasDocument).findFirst().orElse(null);
Slide viewOnceSlide = slideDeck.getSlides().stream().filter(Slide::hasViewOnce).findFirst().orElse(null);
@@ -356,13 +279,13 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
}
glideRequests.load(new DecryptableUri(imageVideoSlide.getUri()))
.centerCrop()
.override(thumbWidth, thumbHeight)
.override(getContext().getResources().getDimensionPixelSize(R.dimen.quote_thumb_size))
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.into(thumbnailView);
} else if (documentSlide != null){
thumbnailView.setVisibility(GONE);
attachmentContainerView.setVisibility(VISIBLE);
attachmentNameView.setText(documentSlide.getFileName().orElse(""));
attachmentNameView.setText(documentSlide.getFileName().or(""));
} else {
thumbnailView.setVisibility(GONE);
attachmentContainerView.setVisibility(GONE);

View File

@@ -1,7 +1,11 @@
package org.thoughtcrime.securesms.components;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.Uri;
import android.widget.Toast;
import androidx.appcompat.app.AlertDialog;

View File

@@ -16,6 +16,7 @@ import androidx.annotation.Nullable;
import java.util.Arrays;
import kotlin.jvm.functions.Function1;
import kotlin.jvm.functions.Function2;
/**
@@ -120,11 +121,7 @@ public final class RotatableGradientDrawable extends Drawable {
public void draw(Canvas canvas) {
int save = canvas.save();
canvas.rotate(degrees, getBounds().width() / 2f, getBounds().height() / 2f);
int height = fillRect.height();
int width = fillRect.width();
canvas.drawRect(fillRect.left - width, fillRect.top - height, fillRect.right + width, fillRect.bottom + height, fillPaint);
canvas.drawRect(fillRect, fillPaint);
canvas.restoreToCount(save);
}

View File

@@ -12,9 +12,7 @@ import org.thoughtcrime.securesms.TransportOptions;
import org.thoughtcrime.securesms.TransportOptions.OnTransportChangedListener;
import org.thoughtcrime.securesms.TransportOptionsPopup;
import org.thoughtcrime.securesms.util.ViewUtil;
import java.util.Optional;
import org.whispersystems.libsignal.util.guava.Optional;
public class SendButton extends AppCompatImageButton
implements TransportOptions.OnTransportChangedListener,
@@ -24,7 +22,7 @@ public class SendButton extends AppCompatImageButton
private final TransportOptions transportOptions;
private Optional<TransportOptionsPopup> transportOptionsPopup = Optional.empty();
private Optional<TransportOptionsPopup> transportOptionsPopup = Optional.absent();
@SuppressWarnings("unused")
public SendButton(Context context) {

View File

@@ -5,6 +5,10 @@ import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.ShapeDrawable;
import android.graphics.drawable.shapes.RoundRectShape;
import android.graphics.drawable.shapes.Shape;
import android.net.Uri;
import android.os.Build;
import android.util.AttributeSet;
@@ -15,6 +19,7 @@ import android.widget.ImageView;
import androidx.annotation.NonNull;
import androidx.annotation.UiThread;
import androidx.core.content.ContextCompat;
import com.bumptech.glide.RequestBuilder;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
@@ -28,23 +33,26 @@ import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.blurhash.BlurHash;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4PlaybackPolicy;
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
import org.thoughtcrime.securesms.mms.GlideRequest;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.mms.SlideClickListener;
import org.thoughtcrime.securesms.mms.SlidesClickedListener;
import org.thoughtcrime.securesms.stories.StoryTextPostModel;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
import org.thoughtcrime.securesms.util.concurrent.SettableFuture;
import org.thoughtcrime.securesms.util.views.Stub;
import org.thoughtcrime.securesms.video.VideoPlayer;
import org.whispersystems.libsignal.util.guava.Optional;
import java.util.Arrays;
import java.util.Collections;
import java.util.Locale;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import static com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade;
@@ -69,7 +77,7 @@ public class ThumbnailView extends FrameLayout {
private final int[] bounds = new int[4];
private final int[] measureDimens = new int[2];
private Optional<TransferControlView> transferControls = Optional.empty();
private Optional<TransferControlView> transferControls = Optional.absent();
private SlideClickListener thumbnailClickListener = null;
private SlidesClickedListener downloadClickListener = null;
private Slide slide = null;
@@ -392,31 +400,6 @@ public class ThumbnailView extends FrameLayout {
return future;
}
public ListenableFuture<Boolean> setImageResource(@NonNull GlideRequests glideRequests, @NonNull StoryTextPostModel model, int width, int height) {
SettableFuture<Boolean> future = new SettableFuture<>();
if (transferControls.isPresent()) getTransferControls().setVisibility(View.GONE);
GlideRequest request = glideRequests.load(model)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.transition(withCrossFade());
if (width > 0 && height > 0) {
request = request.override(width, height);
}
if (radius > 0) {
request = request.transforms(new CenterCrop(), new RoundedCorners(radius));
} else {
request = request.transforms(new CenterCrop());
}
request.into(new GlideDrawableListeningTarget(image, future));
blurhash.setImageDrawable(null);
return future;
}
public void setThumbnailClickListener(SlideClickListener listener) {
this.thumbnailClickListener = listener;
}

View File

@@ -42,12 +42,12 @@ import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.BitmapUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.util.guava.Optional;
import java.io.IOException;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
@SuppressWarnings("deprecation")
public class CameraView extends ViewGroup {
@@ -56,8 +56,8 @@ public class CameraView extends ViewGroup {
private final CameraSurfaceView surface;
private final OnOrientationChange onOrientationChange;
private volatile Optional<Camera> camera = Optional.empty();
private volatile int cameraId = CameraInfo.CAMERA_FACING_BACK;
private volatile Optional<Camera> camera = Optional.absent();
private volatile int cameraId = CameraInfo.CAMERA_FACING_BACK;
private volatile int displayOrientation = -1;
private @NonNull State state = State.PAUSED;
@@ -104,7 +104,7 @@ public class CameraView extends ViewGroup {
Void onRunBackground() {
try {
long openStartMillis = System.currentTimeMillis();
camera = Optional.ofNullable(Camera.open(cameraId));
camera = Optional.fromNullable(Camera.open(cameraId));
Log.i(TAG, "camera.open() -> " + (System.currentTimeMillis() - openStartMillis) + "ms");
synchronized (CameraView.this) {
CameraView.this.notifyAll();
@@ -145,7 +145,7 @@ public class CameraView extends ViewGroup {
@Override
protected void onPreMain() {
cameraToDestroy = camera;
camera = Optional.empty();
camera = Optional.absent();
}
@Override

View File

@@ -14,6 +14,7 @@ import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.emoji.EmojiProvider.EmojiDrawable;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
public class EmojiEditText extends AppCompatEditText {

View File

@@ -1,39 +1,26 @@
package org.thoughtcrime.securesms.components.emoji;
import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.AppCompatImageView;
import org.thoughtcrime.securesms.R;
public class EmojiImageView extends AppCompatImageView {
private final boolean forceJumboEmoji;
public EmojiImageView(Context context) {
this(context, null);
super(context);
}
public EmojiImageView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public EmojiImageView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.EmojiImageView, 0, 0);
forceJumboEmoji = a.getBoolean(R.styleable.EmojiImageView_forceJumbo, false);
a.recycle();
super(context, attrs);
}
public void setImageEmoji(CharSequence emoji) {
if (isInEditMode()) {
setImageResource(R.drawable.ic_emoji);
} else {
setImageDrawable(EmojiProvider.getEmojiDrawable(getContext(), emoji, forceJumboEmoji));
setImageDrawable(EmojiProvider.getEmojiDrawable(getContext(), emoji));
}
}
}

View File

@@ -21,8 +21,8 @@ import org.thoughtcrime.securesms.components.emoji.EmojiPageViewGridAdapter.Emoj
import org.thoughtcrime.securesms.components.emoji.EmojiPageViewGridAdapter.VariationSelectorListener;
import org.thoughtcrime.securesms.util.ContextUtil;
import org.thoughtcrime.securesms.util.DrawableUtil;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel;
import org.thoughtcrime.securesms.util.ViewUtil;
import java.util.List;
import java.util.Optional;

View File

@@ -19,6 +19,7 @@ import org.signal.core.util.ThreadUtil;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.components.emoji.parsing.EmojiDrawInfo;
import org.thoughtcrime.securesms.components.emoji.parsing.EmojiParser;
import org.thoughtcrime.securesms.emoji.EmojiFiles;
import org.thoughtcrime.securesms.emoji.EmojiPageCache;
import org.thoughtcrime.securesms.emoji.EmojiSource;
import org.thoughtcrime.securesms.emoji.JumboEmoji;

View File

@@ -13,7 +13,7 @@ import android.text.TextDirectionHeuristic;
import android.text.TextDirectionHeuristics;
import android.text.TextUtils;
import android.text.method.TransformationMethod;
import android.text.style.CharacterStyle;
import android.text.style.MetricAffectingSpan;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.ViewGroup;
@@ -33,9 +33,9 @@ import org.thoughtcrime.securesms.components.mention.MentionRendererDelegate;
import org.thoughtcrime.securesms.emoji.JumboEmoji;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.util.guava.Optional;
import java.util.List;
import java.util.Optional;
import kotlin.Unit;
@@ -141,7 +141,7 @@ public class EmojiTextView extends AppCompatTextView {
previousTransformationMethod = getTransformationMethod();
if (useSystemEmoji || candidates == null || candidates.size() == 0) {
super.setText(new SpannableStringBuilder(Optional.ofNullable(text).orElse("")), BufferType.SPANNABLE);
super.setText(new SpannableStringBuilder(Optional.fromNullable(text).or("")), BufferType.SPANNABLE);
} else {
CharSequence emojified = EmojiProvider.emojify(candidates, text, this, isJumbomoji || forceJumboEmoji);
super.setText(new SpannableStringBuilder(emojified), BufferType.SPANNABLE);
@@ -149,7 +149,7 @@ public class EmojiTextView extends AppCompatTextView {
// Android fails to ellipsize spannable strings. (https://issuetracker.google.com/issues/36991688)
// We ellipsize them ourselves by manually truncating the appropriate section.
if (getText() != null && getText().length() > 0 && isEllipsizedAtEnd()) {
if (getText() != null && getText().length() > 0 && getEllipsize() == TextUtils.TruncateAt.END) {
if (maxLength > 0) {
ellipsizeAnyTextForMaxLength();
} else if (getMaxLines() > 0) {
@@ -162,17 +162,6 @@ public class EmojiTextView extends AppCompatTextView {
}
}
/**
* Used to determine whether to apply custom ellipsizing logic without necessarily having the
* ellipsize property set. This allows us to work around implementations of Layout which apply an
* ellipsis even when maxLines is not set.
*/
private boolean isEllipsizedAtEnd() {
return getEllipsize() == TextUtils.TruncateAt.END ||
(getMaxLines() > 0 && getMaxLines() < Integer.MAX_VALUE) ||
maxLength > 0;
}
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
widthMeasureSpec = applyWidthMeasureRoundingFix(widthMeasureSpec);
@@ -197,16 +186,16 @@ public class EmojiTextView extends AppCompatTextView {
}
/**
* Starting from API 30, there can be a rounding error in text layout when a non-zero letter
* spacing is used. This causes a line break to be inserted where there shouldn't be one. Force
* the width to be larger to work around this problem.
* Starting from API 30, there can be a rounding error in text layout when a non-default font
* scale is used. This causes a line break to be inserted where there shouldn't be one. Force the
* width to be larger to work around this problem.
* https://issuetracker.google.com/issues/173574230
*
* @param widthMeasureSpec the original measure spec passed to {@link #onMeasure(int, int)}
* @return the measure spec with the workaround, or the original one.
*/
private int applyWidthMeasureRoundingFix(int widthMeasureSpec) {
if (Build.VERSION.SDK_INT >= 30 && getLetterSpacing() > 0) {
if (Build.VERSION.SDK_INT >= 30 && Math.abs(getResources().getConfiguration().fontScale - 1f) > 0.01f) {
CharSequence text = getText();
if (text != null) {
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
@@ -229,7 +218,7 @@ public class EmojiTextView extends AppCompatTextView {
return false;
}
return ((Spanned) text).nextSpanTransition(-1, text.length(), CharacterStyle.class) != text.length();
return ((Spanned) text).nextSpanTransition(-1, text.length(), MetricAffectingSpan.class) != text.length();
}
public int getLastLineWidth() {
@@ -294,15 +283,14 @@ public class EmojiTextView extends AppCompatTextView {
int lineCount = getLineCount();
if (lineCount > maxLines) {
int overflowStart = getLayout().getLineStart(maxLines - 1);
int overflowEnd = getLayout().getLineEnd(maxLines - 1);
CharSequence overflow = getText().subSequence(overflowStart, overflowEnd);
CharSequence overflow = getText().subSequence(overflowStart, getText().length());
float adjust = overflowText != null ? getPaint().measureText(overflowText, 0, overflowText.length()) : 0f;
CharSequence ellipsized = TextUtils.ellipsize(overflow, getPaint(), getWidth() - adjust, TextUtils.TruncateAt.END);
SpannableStringBuilder newContent = new SpannableStringBuilder();
newContent.append(getText().subSequence(0, overflowStart))
.append(ellipsized.subSequence(0, ellipsized.length()))
.append(Optional.ofNullable(overflowText).orElse(""));
.append(Optional.fromNullable(overflowText).or(""));
EmojiParser.CandidateList newCandidates = isInEditMode() ? null : EmojiProvider.getCandidates(newContent);
CharSequence emojified = EmojiProvider.emojify(newCandidates, newContent, this, isJumbomoji || forceJumboEmoji);
@@ -314,7 +302,7 @@ public class EmojiTextView extends AppCompatTextView {
if (getLayout() != null) {
ellipsize.run();
} else {
ViewKt.doOnPreDraw(this, view -> {
ViewKt.doOnNextLayout(this, view -> {
ellipsize.run();
return Unit.INSTANCE;
});

View File

@@ -1,7 +1,6 @@
package org.thoughtcrime.securesms.components.emoji;
import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.view.ContextThemeWrapper;
import android.view.LayoutInflater;
@@ -21,7 +20,8 @@ import org.thoughtcrime.securesms.components.InputAwareLayout.InputView;
import org.thoughtcrime.securesms.keyboard.KeyboardPage;
import org.thoughtcrime.securesms.keyboard.KeyboardPagerFragment;
import org.thoughtcrime.securesms.keyboard.emoji.search.EmojiSearchFragment;
import org.thoughtcrime.securesms.util.ThemedFragment;
import java.util.Objects;
public class MediaKeyboard extends FrameLayout implements InputView {
@@ -34,7 +34,6 @@ public class MediaKeyboard extends FrameLayout implements InputView {
private State keyboardState;
private KeyboardPagerFragment keyboardPagerFragment;
private FragmentManager fragmentManager;
private int mediaKeyboardTheme;
public MediaKeyboard(Context context) {
this(context, null);
@@ -42,12 +41,6 @@ public class MediaKeyboard extends FrameLayout implements InputView {
public MediaKeyboard(Context context, AttributeSet attrs) {
super(context, attrs);
if (attrs != null) {
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.MediaKeyboard);
mediaKeyboardTheme = array.getResourceId(R.styleable.MediaKeyboard_media_keyboard_theme, -1);
array.recycle();
}
}
public void setFragmentManager(@NonNull FragmentManager fragmentManager) {
@@ -77,10 +70,6 @@ public class MediaKeyboard extends FrameLayout implements InputView {
show();
}
public boolean isInitialised() {
return isInitialised;
}
public void show() {
if (!isInitialised) initView();
@@ -133,14 +122,9 @@ public class MediaKeyboard extends FrameLayout implements InputView {
keyboardState = State.EMOJI_SEARCH;
EmojiSearchFragment emojiSearchFragment = new EmojiSearchFragment();
if (mediaKeyboardTheme != -1) {
ThemedFragment.withTheme(emojiSearchFragment, mediaKeyboardTheme);
}
fragmentManager.beginTransaction()
.hide(keyboardPagerFragment)
.add(R.id.media_keyboard_fragment_container, emojiSearchFragment, EMOJI_SEARCH)
.add(R.id.media_keyboard_fragment_container, new EmojiSearchFragment(), EMOJI_SEARCH)
.runOnCommit(() -> show(latestKeyboardHeight, true))
.setCustomAnimations(R.anim.fade_in, R.anim.fade_out)
.commitAllowingStateLoss();
@@ -157,10 +141,6 @@ public class MediaKeyboard extends FrameLayout implements InputView {
}
keyboardPagerFragment = new KeyboardPagerFragment();
if (mediaKeyboardTheme != -1) {
ThemedFragment.withTheme(keyboardPagerFragment, mediaKeyboardTheme);
}
fragmentManager.beginTransaction()
.replace(R.id.media_keyboard_fragment_container, keyboardPagerFragment)
.commitNowAllowingStateLoss();

View File

@@ -6,7 +6,7 @@ import android.util.AttributeSet
import androidx.appcompat.widget.AppCompatTextView
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.util.ThrottledDebouncer
import java.util.Optional
import org.whispersystems.libsignal.util.guava.Optional
open class SimpleEmojiTextView @JvmOverloads constructor(
context: Context,
@@ -21,7 +21,7 @@ open class SimpleEmojiTextView @JvmOverloads constructor(
bufferType = type
val candidates = if (isInEditMode) null else EmojiProvider.getCandidates(text)
if (SignalStore.settings().isPreferSystemEmoji || candidates == null || candidates.size() == 0) {
super.setText(Optional.ofNullable(text).orElse(""), type)
super.setText(Optional.fromNullable(text).or(""), type)
} else {
val startDrawableSize: Int = compoundDrawables[0]?.let { it.intrinsicWidth + compoundDrawablePadding } ?: 0
val endDrawableSize: Int = compoundDrawables[1]?.let { it.intrinsicWidth + compoundDrawablePadding } ?: 0

View File

@@ -9,7 +9,7 @@ import androidx.appcompat.app.AlertDialog;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.crypto.ReentrantSessionLock;
import org.thoughtcrime.securesms.crypto.storage.SignalIdentityKeyStore;
import org.thoughtcrime.securesms.crypto.storage.TextSecureIdentityKeyStore;
import org.thoughtcrime.securesms.database.model.IdentityRecord;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
@@ -40,7 +40,7 @@ public class UntrustedSendDialog extends AlertDialog.Builder implements DialogIn
@Override
public void onClick(DialogInterface dialog, int which) {
final SignalIdentityKeyStore identityStore = ApplicationDependencies.getProtocolStore().aci().identities();
final TextSecureIdentityKeyStore identityStore = ApplicationDependencies.getIdentityStore();
SimpleTask.run(() -> {
try(SignalSessionLock.Lock unused = ReentrantSessionLock.INSTANCE.acquire()) {

View File

@@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.components.identity;
import android.content.Context;
import android.content.DialogInterface;
import android.os.AsyncTask;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
@@ -42,9 +43,9 @@ public class UnverifiedSendDialog extends AlertDialog.Builder implements DialogI
SimpleTask.run(() -> {
try(SignalSessionLock.Lock unused = ReentrantSessionLock.INSTANCE.acquire()) {
for (IdentityRecord identityRecord : untrustedRecords) {
ApplicationDependencies.getProtocolStore().aci().identities().setVerified(identityRecord.getRecipientId(),
identityRecord.getIdentityKey(),
IdentityDatabase.VerifiedStatus.DEFAULT);
ApplicationDependencies.getIdentityStore().setVerified(identityRecord.getRecipientId(),
identityRecord.getIdentityKey(),
IdentityDatabase.VerifiedStatus.DEFAULT);
}
}
return null;

View File

@@ -1,91 +0,0 @@
package org.thoughtcrime.securesms.components.menu
import android.os.Build
import android.view.View
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel
import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder
/**
* Handles the setup and display of actions shown in a context menu.
*/
class ContextMenuList(recyclerView: RecyclerView, onItemClick: () -> Unit) {
private val mappingAdapter = MappingAdapter().apply {
registerFactory(DisplayItem::class.java, LayoutFactory({ ItemViewHolder(it, onItemClick) }, R.layout.signal_context_menu_item))
}
init {
recyclerView.apply {
adapter = mappingAdapter
layoutManager = LinearLayoutManager(context)
itemAnimator = null
}
}
fun setItems(items: List<ActionItem>) {
mappingAdapter.submitList(items.toAdapterItems())
}
private fun List<ActionItem>.toAdapterItems(): List<DisplayItem> {
return this.mapIndexed { index, item ->
val displayType: DisplayType = when {
this.size == 1 -> DisplayType.ONLY
index == 0 -> DisplayType.TOP
index == this.size - 1 -> DisplayType.BOTTOM
else -> DisplayType.MIDDLE
}
DisplayItem(item, displayType)
}
}
private data class DisplayItem(
val item: ActionItem,
val displayType: DisplayType
) : MappingModel<DisplayItem> {
override fun areItemsTheSame(newItem: DisplayItem): Boolean {
return this == newItem
}
override fun areContentsTheSame(newItem: DisplayItem): Boolean {
return this == newItem
}
}
private enum class DisplayType {
TOP, BOTTOM, MIDDLE, ONLY
}
private class ItemViewHolder(
itemView: View,
private val onItemClick: () -> Unit,
) : MappingViewHolder<DisplayItem>(itemView) {
val icon: ImageView = itemView.findViewById(R.id.signal_context_menu_item_icon)
val title: TextView = itemView.findViewById(R.id.signal_context_menu_item_title)
override fun bind(model: DisplayItem) {
icon.setImageResource(model.item.iconRes)
title.text = model.item.title
itemView.setOnClickListener {
model.item.action.run()
onItemClick()
}
if (Build.VERSION.SDK_INT >= 21) {
when (model.displayType) {
DisplayType.TOP -> itemView.setBackgroundResource(R.drawable.signal_context_menu_item_background_top)
DisplayType.BOTTOM -> itemView.setBackgroundResource(R.drawable.signal_context_menu_item_background_bottom)
DisplayType.MIDDLE -> itemView.setBackgroundResource(R.drawable.signal_context_menu_item_background_middle)
DisplayType.ONLY -> itemView.setBackgroundResource(R.drawable.signal_context_menu_item_background_only)
}
}
}
}
}

View File

@@ -6,10 +6,18 @@ import android.os.Build
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.PopupWindow
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.util.ViewUtil
import org.thoughtcrime.securesms.util.adapter.mapping.Factory
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel
import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder
/**
* A custom context menu that will show next to an anchor view and display several options. Basically a PopupMenu with custom UI and positioning rules.
@@ -34,10 +42,9 @@ class SignalContextMenu private constructor(
val context: Context = anchor.context
private val contextMenuList = ContextMenuList(
recyclerView = contentView.findViewById(R.id.signal_context_menu_list),
onItemClick = { dismiss() },
)
val mappingAdapter = MappingAdapter().apply {
registerFactory(DisplayItem::class.java, ItemViewHolderFactory())
}
init {
setBackgroundDrawable(ContextCompat.getDrawable(context, R.drawable.signal_context_menu_background))
@@ -52,7 +59,13 @@ class SignalContextMenu private constructor(
elevation = 20f
}
contextMenuList.setItems(items)
contentView.findViewById<RecyclerView>(R.id.signal_context_menu_list).apply {
adapter = mappingAdapter
layoutManager = LinearLayoutManager(context)
itemAnimator = null
}
mappingAdapter.submitList(items.toAdapterItems())
}
private fun show() {
@@ -84,7 +97,7 @@ class SignalContextMenu private constructor(
offsetY = baseOffsetY
} else if (menuTopBound > screenTopBound) {
offsetY = -(anchorRect.height() + contentView.measuredHeight + baseOffsetY)
contextMenuList.setItems(items.reversed())
mappingAdapter.submitList(items.reversed().toAdapterItems())
} else {
offsetY = -((anchorRect.height() / 2) + (contentView.measuredHeight / 2) + baseOffsetY)
}
@@ -109,6 +122,65 @@ class SignalContextMenu private constructor(
showAsDropDown(anchor, offsetX, offsetY)
}
private fun List<ActionItem>.toAdapterItems(): List<DisplayItem> {
return this.mapIndexed { index, item ->
val displayType: DisplayType = when {
this.size == 1 -> DisplayType.ONLY
index == 0 -> DisplayType.TOP
index == this.size - 1 -> DisplayType.BOTTOM
else -> DisplayType.MIDDLE
}
DisplayItem(item, displayType)
}
}
private data class DisplayItem(
val item: ActionItem,
val displayType: DisplayType
) : MappingModel<DisplayItem> {
override fun areItemsTheSame(newItem: DisplayItem): Boolean {
return this == newItem
}
override fun areContentsTheSame(newItem: DisplayItem): Boolean {
return this == newItem
}
}
private enum class DisplayType {
TOP, BOTTOM, MIDDLE, ONLY
}
private inner class ItemViewHolder(itemView: View) : MappingViewHolder<DisplayItem>(itemView) {
val icon: ImageView = itemView.findViewById(R.id.signal_context_menu_item_icon)
val title: TextView = itemView.findViewById(R.id.signal_context_menu_item_title)
override fun bind(model: DisplayItem) {
icon.setImageResource(model.item.iconRes)
title.text = model.item.title
itemView.setOnClickListener {
model.item.action.run()
dismiss()
}
if (Build.VERSION.SDK_INT >= 21) {
when (model.displayType) {
DisplayType.TOP -> itemView.setBackgroundResource(R.drawable.signal_context_menu_item_background_top)
DisplayType.BOTTOM -> itemView.setBackgroundResource(R.drawable.signal_context_menu_item_background_bottom)
DisplayType.MIDDLE -> itemView.setBackgroundResource(R.drawable.signal_context_menu_item_background_middle)
DisplayType.ONLY -> itemView.setBackgroundResource(R.drawable.signal_context_menu_item_background_only)
}
}
}
}
private inner class ItemViewHolderFactory : Factory<DisplayItem> {
override fun createViewHolder(parent: ViewGroup): MappingViewHolder<DisplayItem> {
return ItemViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.signal_context_menu_item, parent, false))
}
}
enum class HorizontalPosition {
START, END
}

View File

@@ -8,6 +8,7 @@ import android.net.Uri;
import android.os.Build;
import android.os.PowerManager;
import android.provider.Settings;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;

View File

@@ -5,6 +5,7 @@ import android.content.Context;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.registration.RegistrationNavigationActivity;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
public class PushRegistrationReminder extends Reminder {

View File

@@ -1,52 +0,0 @@
/*
MIT License
Copyright (c) 2020 Tiago Ornelas
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
package org.thoughtcrime.securesms.components.segmentedprogressbar
/**
* Created by Tiago Ornelas on 18/04/2020.
* Model that holds the segment state
*/
class Segment(val animationDurationMillis: Long) {
var animationProgressPercentage: Float = 0f
var animationState: AnimationState = AnimationState.IDLE
set(value) {
animationProgressPercentage = when (value) {
AnimationState.ANIMATED -> 1f
AnimationState.IDLE -> 0f
else -> animationProgressPercentage
}
field = value
}
/**
* Represents possible drawing states of the segment
*/
enum class AnimationState {
ANIMATED,
ANIMATING,
IDLE
}
}

View File

@@ -1,6 +0,0 @@
package org.thoughtcrime.securesms.components.segmentedprogressbar
data class SegmentState(
val position: Long,
val duration: Long
)

View File

@@ -1,416 +0,0 @@
/*
MIT License
Copyright (c) 2020 Tiago Ornelas
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
package org.thoughtcrime.securesms.components.segmentedprogressbar
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Path
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import androidx.viewpager.widget.ViewPager
import org.thoughtcrime.securesms.R
import java.util.concurrent.TimeUnit
/**
* Created by Tiago Ornelas on 18/04/2020.
* Represents a segmented progress bar on which, the progress is set by segments
* @see Segment
* And the progress of each segment is animated based on a set speed
*/
class SegmentedProgressBar : View, ViewPager.OnPageChangeListener, View.OnTouchListener {
companion object {
/**
* It is common now for devices to run at 60FPS
*/
val MILLIS_PER_FRAME = TimeUnit.MILLISECONDS.toMillis(17)
}
private val path = Path()
private val corners = floatArrayOf(0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f)
/**
* Number of total segments to draw
*/
var segmentCount: Int = resources.getInteger(R.integer.segmentedprogressbar_default_segments_count)
set(value) {
field = value
this.initSegments()
}
/**
* Mapping of segment index -> duration in millis. Negative durations
* ARE valid but they'll result in a call to SegmentedProgressBarListener#onRequestSegmentProgressPercentage
* which should return the current % position for the currently playing item. This helps
* to avoid synchronizing the seek bar to playback.
*/
var segmentDurations: Map<Int, Long> = mapOf()
set(value) {
field = value
this.initSegments()
}
var margin: Int = resources.getDimensionPixelSize(R.dimen.segmentedprogressbar_default_segment_margin)
private set
var radius: Int = resources.getDimensionPixelSize(R.dimen.segmentedprogressbar_default_corner_radius)
private set
var segmentStrokeWidth: Int =
resources.getDimensionPixelSize(R.dimen.segmentedprogressbar_default_segment_stroke_width)
private set
var segmentBackgroundColor: Int = Color.WHITE
private set
var segmentSelectedBackgroundColor: Int =
context.getThemeColor(R.attr.colorAccent)
private set
var segmentStrokeColor: Int = Color.BLACK
private set
var segmentSelectedStrokeColor: Int = Color.BLACK
private set
var timePerSegmentMs: Long =
resources.getInteger(R.integer.segmentedprogressbar_default_time_per_segment_ms).toLong()
private set
private var segments = mutableListOf<Segment>()
private val selectedSegment: Segment?
get() = segments.firstOrNull { it.animationState == Segment.AnimationState.ANIMATING }
private val selectedSegmentIndex: Int
get() = segments.indexOf(this.selectedSegment)
// Drawing
val strokeApplicable: Boolean
get() = segmentStrokeWidth * 4 <= measuredHeight
val segmentWidth: Float
get() = (measuredWidth - margin * (segmentCount - 1)).toFloat() / segmentCount
var viewPager: ViewPager? = null
@SuppressLint("ClickableViewAccessibility")
set(value) {
field = value
if (value == null) {
viewPager?.removeOnPageChangeListener(this)
viewPager?.setOnTouchListener(null)
} else {
viewPager?.addOnPageChangeListener(this)
viewPager?.setOnTouchListener(this)
}
}
/**
* Sets callbacks for progress bar state changes
* @see SegmentedProgressBarListener
*/
var listener: SegmentedProgressBarListener? = null
private var lastFrameTimeMillis: Long = 0L
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
val typedArray =
context.theme.obtainStyledAttributes(attrs, R.styleable.SegmentedProgressBar, 0, 0)
segmentCount =
typedArray.getInt(R.styleable.SegmentedProgressBar_totalSegments, segmentCount)
margin =
typedArray.getDimensionPixelSize(
R.styleable.SegmentedProgressBar_segmentMargins,
margin
)
radius =
typedArray.getDimensionPixelSize(
R.styleable.SegmentedProgressBar_segmentCornerRadius,
radius
)
segmentStrokeWidth =
typedArray.getDimensionPixelSize(
R.styleable.SegmentedProgressBar_segmentStrokeWidth,
segmentStrokeWidth
)
segmentBackgroundColor =
typedArray.getColor(
R.styleable.SegmentedProgressBar_segmentBackgroundColor,
segmentBackgroundColor
)
segmentSelectedBackgroundColor =
typedArray.getColor(
R.styleable.SegmentedProgressBar_segmentSelectedBackgroundColor,
segmentSelectedBackgroundColor
)
segmentStrokeColor =
typedArray.getColor(
R.styleable.SegmentedProgressBar_segmentStrokeColor,
segmentStrokeColor
)
segmentSelectedStrokeColor =
typedArray.getColor(
R.styleable.SegmentedProgressBar_segmentSelectedStrokeColor,
segmentSelectedStrokeColor
)
timePerSegmentMs =
typedArray.getInt(
R.styleable.SegmentedProgressBar_timePerSegment,
timePerSegmentMs.toInt()
).toLong()
typedArray.recycle()
}
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(
context,
attrs,
defStyleAttr
)
init {
setLayerType(LAYER_TYPE_SOFTWARE, null)
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
segments.forEachIndexed { index, segment ->
val drawingComponents = getDrawingComponents(segment, index)
when (index) {
0 -> {
corners.indices.forEach { corners[it] = 0f }
corners[0] = radius.toFloat()
corners[1] = radius.toFloat()
corners[6] = radius.toFloat()
corners[7] = radius.toFloat()
}
segments.lastIndex -> {
corners.indices.forEach { corners[it] = 0f }
corners[2] = radius.toFloat()
corners[3] = radius.toFloat()
corners[4] = radius.toFloat()
corners[5] = radius.toFloat()
}
}
drawingComponents.first.forEachIndexed { drawingIndex, rectangle ->
when (index) {
0, segments.lastIndex -> {
path.reset()
path.addRoundRect(rectangle, corners, Path.Direction.CW)
canvas?.drawPath(path, drawingComponents.second[drawingIndex])
}
else -> canvas?.drawRect(
rectangle,
drawingComponents.second[drawingIndex]
)
}
}
}
onFrame(System.currentTimeMillis())
}
/**
* Start/Resume progress animation
*/
fun start() {
pause()
val segment = selectedSegment
if (segment == null) {
next()
} else {
isPaused = false
invalidate()
}
}
/**
* Pauses the animation process
*/
fun pause() {
isPaused = true
lastFrameTimeMillis = 0L
}
/**
* Resets the whole animation state and selected segments
* !Doesn't restart it!
* To restart, call the start() method
*/
fun reset() {
this.segments.map { it.animationState = Segment.AnimationState.IDLE }
this.invalidate()
}
/**
* Starts animation for the following segment
*/
fun next() {
loadSegment(offset = 1, userAction = true)
}
/**
* Starts animation for the previous segment
*/
fun previous() {
loadSegment(offset = -1, userAction = true)
}
/**
* Restarts animation for the current segment
*/
fun restartSegment() {
loadSegment(offset = 0, userAction = true)
}
/**
* Skips a number of segments
* @param offset number o segments fo skip
*/
fun skip(offset: Int) {
loadSegment(offset = offset, userAction = true)
}
/**
* Sets current segment to the
* @param position index
*/
fun setPosition(position: Int) {
loadSegment(offset = position - this.selectedSegmentIndex, userAction = true)
}
// Private methods
private fun loadSegment(offset: Int, userAction: Boolean) {
val oldSegmentIndex = this.segments.indexOf(this.selectedSegment)
val nextSegmentIndex = oldSegmentIndex + offset
// Index out of bounds, ignore operation
if (userAction && nextSegmentIndex !in 0 until segmentCount) {
if (nextSegmentIndex >= segmentCount) {
this.listener?.onFinished()
} else {
restartSegment()
}
return
}
segments.mapIndexed { index, segment ->
if (offset > 0) {
if (index < nextSegmentIndex) segment.animationState =
Segment.AnimationState.ANIMATED
} else if (offset < 0) {
if (index > nextSegmentIndex - 1) segment.animationState =
Segment.AnimationState.IDLE
} else if (offset == 0) {
if (index == nextSegmentIndex) segment.animationState = Segment.AnimationState.IDLE
}
}
val nextSegment = this.segments.getOrNull(nextSegmentIndex)
// Handle next segment transition/ending
if (nextSegment != null) {
pause()
nextSegment.animationState = Segment.AnimationState.ANIMATING
isPaused = false
invalidate()
this.listener?.onPage(oldSegmentIndex, this.selectedSegmentIndex)
viewPager?.currentItem = this.selectedSegmentIndex
} else {
pause()
this.listener?.onFinished()
}
}
private fun getSegmentProgressPercentage(segment: Segment, timeSinceLastFrameMillis: Long): Float {
return if (segment.animationDurationMillis > 0) {
segment.animationProgressPercentage + timeSinceLastFrameMillis.toFloat() / segment.animationDurationMillis
} else {
listener?.onRequestSegmentProgressPercentage() ?: 0f
}
}
private fun initSegments() {
this.segments.clear()
segments.addAll(
List(segmentCount) {
val duration = segmentDurations[it] ?: timePerSegmentMs
Segment(duration)
}
)
this.invalidate()
reset()
}
private var isPaused = true
private fun onFrame(frameTimeMillis: Long) {
if (isPaused) {
return
}
val lastFrameTimeMillis = this.lastFrameTimeMillis
this.lastFrameTimeMillis = frameTimeMillis
val selectedSegment = this.selectedSegment
if (selectedSegment == null) {
loadSegment(offset = 1, userAction = false)
} else if (lastFrameTimeMillis > 0L) {
val segmentProgressPercentage = getSegmentProgressPercentage(selectedSegment, frameTimeMillis - lastFrameTimeMillis)
selectedSegment.animationProgressPercentage = segmentProgressPercentage
if (selectedSegment.animationProgressPercentage >= 1f) {
loadSegment(offset = 1, userAction = false)
} else {
this.invalidate()
}
} else {
this.invalidate()
}
}
override fun onPageScrollStateChanged(state: Int) {}
override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {}
override fun onPageSelected(position: Int) {
this.setPosition(position)
}
override fun onTouch(p0: View?, p1: MotionEvent?): Boolean {
when (p1?.action) {
MotionEvent.ACTION_DOWN -> pause()
MotionEvent.ACTION_UP -> start()
}
return false
}
}

View File

@@ -1,42 +0,0 @@
/*
MIT License
Copyright (c) 2020 Tiago Ornelas
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
package org.thoughtcrime.securesms.components.segmentedprogressbar
/**
* Created by Tiago Ornelas on 18/04/2020.
* Interface to communicate progress events
*/
interface SegmentedProgressBarListener {
/**
* Notifies when selected segment changed
*/
fun onPage(oldPageIndex: Int, newPageIndex: Int)
/**
* Notifies when last segment finished animating
*/
fun onFinished()
fun onRequestSegmentProgressPercentage(): Float?
}

View File

@@ -1,95 +0,0 @@
/*
MIT License
Copyright (c) 2020 Tiago Ornelas
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
package org.thoughtcrime.securesms.components.segmentedprogressbar
import android.content.Context
import android.graphics.Paint
import android.graphics.RectF
import android.util.TypedValue
fun Context.getThemeColor(attributeColor: Int): Int {
val typedValue = TypedValue()
this.theme.resolveAttribute(attributeColor, typedValue, true)
return typedValue.data
}
fun SegmentedProgressBar.getDrawingComponents(
segment: Segment,
segmentIndex: Int
): Pair<MutableList<RectF>, MutableList<Paint>> {
val rectangles = mutableListOf<RectF>()
val paints = mutableListOf<Paint>()
val segmentWidth = segmentWidth
val startBound = segmentIndex * segmentWidth + ((segmentIndex) * margin)
val endBound = startBound + segmentWidth
val stroke = if (!strokeApplicable) 0f else this.segmentStrokeWidth.toFloat()
val backgroundPaint = Paint().apply {
style = Paint.Style.FILL
color = segmentBackgroundColor
}
val selectedBackgroundPaint = Paint().apply {
style = Paint.Style.FILL
color = segmentSelectedBackgroundColor
}
val strokePaint = Paint().apply {
color =
if (segment.animationState == Segment.AnimationState.IDLE) segmentStrokeColor else segmentSelectedStrokeColor
style = Paint.Style.STROKE
strokeWidth = stroke
}
// Background component
if (segment.animationState == Segment.AnimationState.ANIMATED) {
rectangles.add(RectF(startBound + stroke, height - stroke, endBound - stroke, stroke))
paints.add(selectedBackgroundPaint)
} else {
rectangles.add(RectF(startBound + stroke, height - stroke, endBound - stroke, stroke))
paints.add(backgroundPaint)
}
// Progress component
if (segment.animationState == Segment.AnimationState.ANIMATING) {
rectangles.add(
RectF(
startBound + stroke,
height - stroke,
startBound + segment.animationProgressPercentage * segmentWidth,
stroke
)
)
paints.add(selectedBackgroundPaint)
}
// Stroke component
if (stroke > 0) {
rectangles.add(RectF(startBound + stroke, height - stroke, endBound - stroke, stroke))
paints.add(strokePaint)
}
return Pair(rectangles, paints)
}

View File

@@ -22,47 +22,38 @@ abstract class DSLSettingsFragment(
@StringRes private val titleId: Int = -1,
@MenuRes private val menuId: Int = -1,
@LayoutRes layoutId: Int = R.layout.dsl_settings_fragment,
protected var layoutManagerProducer: (Context) -> RecyclerView.LayoutManager = { context -> LinearLayoutManager(context) }
val layoutManagerProducer: (Context) -> RecyclerView.LayoutManager = { context -> LinearLayoutManager(context) }
) : Fragment(layoutId) {
protected var recyclerView: RecyclerView? = null
private set
private var recyclerView: RecyclerView? = null
private var scrollAnimationHelper: OnScrollAnimationHelper? = null
@CallSuper
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val toolbar: Toolbar? = view.findViewById(R.id.toolbar)
val toolbarShadow: View? = view.findViewById(R.id.toolbar_shadow)
val toolbar: Toolbar = view.findViewById(R.id.toolbar)
val toolbarShadow: View = view.findViewById(R.id.toolbar_shadow)
if (titleId != -1) {
toolbar?.setTitle(titleId)
toolbar.setTitle(titleId)
}
toolbar?.setNavigationOnClickListener {
toolbar.setNavigationOnClickListener {
requireActivity().onBackPressed()
}
if (menuId != -1) {
toolbar?.inflateMenu(menuId)
toolbar?.setOnMenuItemClickListener { onOptionsItemSelected(it) }
}
if (toolbarShadow != null) {
scrollAnimationHelper = getOnScrollAnimationHelper(toolbarShadow)
toolbar.inflateMenu(menuId)
toolbar.setOnMenuItemClickListener { onOptionsItemSelected(it) }
}
scrollAnimationHelper = getOnScrollAnimationHelper(toolbarShadow)
val settingsAdapter = DSLSettingsAdapter()
recyclerView = view.findViewById<RecyclerView>(R.id.recycler).apply {
edgeEffectFactory = EdgeEffectFactory()
layoutManager = layoutManagerProducer(requireContext())
adapter = settingsAdapter
val helper = scrollAnimationHelper
if (helper != null) {
addOnScrollListener(helper)
}
addOnScrollListener(scrollAnimationHelper!!)
}
bindAdapter(settingsAdapter)

View File

@@ -4,11 +4,8 @@ import android.content.Context
import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
import android.graphics.drawable.Drawable
import android.graphics.drawable.InsetDrawable
import android.graphics.drawable.LayerDrawable
import androidx.annotation.ColorRes
import androidx.annotation.DrawableRes
import androidx.annotation.Px
import androidx.core.content.ContextCompat
import org.thoughtcrime.securesms.R
@@ -27,23 +24,6 @@ sealed class DSLSettingsIcon {
}
}
private data class FromResourceWithBackground(
@DrawableRes private val iconId: Int,
@ColorRes private val iconTintId: Int,
@DrawableRes private val backgroundId: Int,
@ColorRes private val backgroundTint: Int,
@Px private val insetPx: Int,
) : DSLSettingsIcon() {
override fun resolve(context: Context): Drawable {
return LayerDrawable(
arrayOf(
FromResource(backgroundId, backgroundTint).resolve(context),
InsetDrawable(FromResource(iconId, iconTintId).resolve(context), insetPx, insetPx, insetPx, insetPx)
)
)
}
}
private data class FromDrawable(
private val drawable: Drawable
) : DSLSettingsIcon() {
@@ -53,17 +33,6 @@ sealed class DSLSettingsIcon {
abstract fun resolve(context: Context): Drawable
companion object {
@JvmStatic
fun from(
@DrawableRes iconId: Int,
@ColorRes iconTintId: Int,
@DrawableRes backgroundId: Int,
@ColorRes backgroundTint: Int,
@Px insetPx: Int = 0
): DSLSettingsIcon {
return FromResourceWithBackground(iconId, iconTintId, backgroundId, backgroundTint, insetPx)
}
@JvmStatic
fun from(@DrawableRes iconId: Int, @ColorRes iconTintId: Int = R.color.signal_icon_tint_primary): DSLSettingsIcon = FromResource(iconId, iconTintId)

View File

@@ -1,7 +1,6 @@
package org.thoughtcrime.securesms.components.settings
import android.content.Context
import android.text.SpannableStringBuilder
import androidx.annotation.ColorInt
import androidx.annotation.StringRes
import androidx.annotation.StyleRes
@@ -82,17 +81,4 @@ sealed class DSLSettingsText {
return SpanUtil.bold(charSequence)
}
}
class LearnMoreModifier(
@ColorInt private val learnMoreColor: Int,
val onClick: () -> Unit
) : Modifier {
override fun modify(context: Context, charSequence: CharSequence): CharSequence {
return SpannableStringBuilder(charSequence).append(" ").append(
SpanUtil.learnMore(context, learnMoreColor) {
onClick()
}
)
}
}
}

View File

@@ -4,7 +4,6 @@ import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.navigation.NavDirections
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import io.reactivex.rxjava3.subjects.PublishSubject
import io.reactivex.rxjava3.subjects.Subject
import org.thoughtcrime.securesms.MainActivity
@@ -16,8 +15,6 @@ import org.thoughtcrime.securesms.components.settings.app.subscription.DonationP
import org.thoughtcrime.securesms.help.HelpFragment
import org.thoughtcrime.securesms.keyvalue.SettingsValues
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.service.KeyCachingService
import org.thoughtcrime.securesms.util.CachedInflater
import org.thoughtcrime.securesms.util.DynamicTheme
@@ -27,7 +24,6 @@ private const val START_LOCATION = "app.settings.start.location"
private const val START_ARGUMENTS = "app.settings.start.arguments"
private const val NOTIFICATION_CATEGORY = "android.intent.category.NOTIFICATION_PREFERENCES"
private const val STATE_WAS_CONFIGURATION_UPDATED = "app.settings.state.configuration.updated"
private const val EXTRA_PERFORM_ACTION_ON_CREATE = "extra_perform_action_on_create"
class AppSettingsActivity : DSLSettingsActivity(), DonationPaymentComponent {
@@ -86,17 +82,6 @@ class AppSettingsActivity : DSLSettingsActivity(), DonationPaymentComponent {
startService(intent)
}
}
if (savedInstanceState == null) {
when (intent.getStringExtra(EXTRA_PERFORM_ACTION_ON_CREATE)) {
ACTION_CHANGE_NUMBER_SUCCESS -> {
MaterialAlertDialogBuilder(this)
.setMessage(getString(R.string.ChangeNumber__your_phone_number_has_changed_to_s, PhoneNumberFormatter.prettyPrint(Recipient.self().requireE164())))
.setPositiveButton(R.string.ChangeNumber__okay, null)
.show()
}
}
}
}
override fun onNewIntent(intent: Intent?) {
@@ -124,14 +109,8 @@ class AppSettingsActivity : DSLSettingsActivity(), DonationPaymentComponent {
}
companion object {
const val ACTION_CHANGE_NUMBER_SUCCESS = "action_change_number_success"
@JvmStatic
@JvmOverloads
fun home(context: Context, action: String? = null): Intent {
return getIntentForStartLocation(context, StartLocation.HOME)
.putExtra(EXTRA_PERFORM_ACTION_ON_CREATE, action)
}
fun home(context: Context): Intent = getIntentForStartLocation(context, StartLocation.HOME)
@JvmStatic
fun backups(context: Context): Intent = getIntentForStartLocation(context, StartLocation.BACKUPS)

View File

@@ -25,7 +25,7 @@ class AppSettingsViewModel(private val subscriptionsRepository: SubscriptionsRep
val state: LiveData<AppSettingsState> = store.stateLiveData
init {
store.update(unreadPaymentsLiveData) { payments, state -> state.copy(unreadPaymentsCount = payments.map { it.unreadCount }.orElse(0)) }
store.update(unreadPaymentsLiveData) { payments, state -> state.copy(unreadPaymentsCount = payments.transform { it.unreadCount }.or(0)) }
store.update(selfLiveData) { self, state -> state.copy(self = self) }
}

View File

@@ -31,6 +31,7 @@ import org.thoughtcrime.securesms.lock.v2.KbsConstants
import org.thoughtcrime.securesms.lock.v2.PinKeyboardType
import org.thoughtcrime.securesms.pin.RegistrationLockV2Dialog
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.FeatureFlags
import org.thoughtcrime.securesms.util.ServiceUtil
import org.thoughtcrime.securesms.util.ThemeUtil
import org.thoughtcrime.securesms.util.navigation.safeNavigate
@@ -106,7 +107,7 @@ class AccountSettingsFragment : DSLSettingsFragment(R.string.AccountSettingsFrag
sectionHeaderPref(R.string.AccountSettingsFragment__account)
if (Recipient.self().changeNumberCapability == Recipient.Capability.SUPPORTED && SignalStore.account().isRegistered) {
if (FeatureFlags.changeNumber() && Recipient.self().changeNumberCapability == Recipient.Capability.SUPPORTED) {
clickPref(
title = DSLSettingsText.from(R.string.AccountSettingsFragment__change_phone_number),
onClick = {

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